why not

[React] Custom Component 본문

CodeStates/블로깅 챌린지

[React] Custom Component

novem 2023. 2. 22. 20:50

1. Styled Components

- 앞서 배운 CSS in JS 라는 개념이 대두되면서 나오게 되었다.

- CSS 코드를 다루면서 불편했던 점을 CSS를 컴포넌트화 시킴으로써 해결해주는 React 환경에서 사용이 가능한 라이브러리이다.

- 기존에 HTML, CSS, JS 파일로 쪼개서 개발하던 방법 -> React 등의 라이브러리의 등장으로 컴포넌트 단위 개발이 주로 이루어졌지만, CSS는 그렇지 못했다는 점에서 출발하였다.

- CSS in JS 라이브러리를 사용하면 CSS도 쉽게 Javascript 안에 넣어줄 수 있으므로, HTML + JS + CSS까지 묶어서 하나의 JS파일 안에서 컴포넌트 단위로 개발할 있다.

- CSS in JS 라이브러리 중에서 현재 가장 인기 있는 라이브러리이다.

1-2. Styled Components 문법

1-2.1) 컴포넌트 만들기

Styled Components는 ES6의 Templete Literals 문법을 사용한다. -> 따옴표가 아닌 백틱(`)을 사용

 

- 컴포넌트를 선언한 후 styled.태그종류를 할당하고, 백틱 안에 기존에 CSS를 작성하던 문법과 똑같이 스타일 속성을 작성

-  이렇게 만든 컴포넌트를 React 컴포넌트를 사용하듯 리턴문 안에 작성해주면 스타일이 적용된 컴포넌트가 렌더되는 것을 확인이 가능하다.

1-2.2) 컴포넌트를 재활용해서 새로운 컴포넌트 만들기

- 컴포넌트를 선언하고 styled() 에 재활용할 컴포넌트를 전달해준 다음, 추가하고 싶은 스타일 속성을 작성해주면 된다.

 

1-2.3) Props 활용하기

- Styled Component로 만든 컴포넌트도 React 컴포넌트처럼 props를 내려줄 수 있다.

- 내려준 props 값에 따라서 컴포넌트를 렌더링하는 것도 가능하다.

- Styled Components는 템플릿 리터럴 문법( ${ } )을 사용하여 JavaScript 코드사용 -> props를 받아오려면 props를 인자로 받는 함수를 만들어 사용

 

2. Bare minimum Requirement

2-1.  Modal Component

 

import { useState } from "react";
import styled from "styled-components";

export const ModalContainer = styled.div`
  // TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
  display: flex;
  // 요소들을 플렉스 박스로 설정
  justify-content: center;
  // 플렉스 박스의 가로 방향 정렬을 가운데로 설정
  align-items: center;
  // 플렉스 박스의 세로 방향 정렬을 가운데로 설정
  height: 100vh;
  // 해당 요소의 높이를 브라우저 창의 세로 높이를 100vh로 설정
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
  // fixed는 보이는 곳에서 고정, absolute는 전체에서 고정
  position: fixed;
  // 해당 요소의 위치를 뷰포트를 기준으로 고정
  display: flex;
  justify-content: center;
  align-items: center;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  // 해당 요소의 위치를 뷰포트의 상단, 좌측, 우측, 하단에 맞게 설정 -> 전체 화면에서 가운데로 고정
  background-color: rgba(0, 0, 0, 0.5);
  // 배경색을 검은색으로 설정하고 0.5의 투명도로 설정
`;

export const ModalBtn = styled.button`
  // 모달 열고 닫기 버튼 구성
  // background-color: var(--coz-purple-600);
  background-color: rgb(237, 219, 199);
  // 해당 요소의 배경색을 사전에 정의된 색상 변수(var)인 --coz-purple-600으로 설정
  font-weight: bold;
  text-decoration: none;
  // 내부의 텍스트의 밑줄을 제거
  border: none;
  // 해당 요소의 테두리를 제거
  padding: 20px;
  // 내부 여백을 20px으로 설정
  color: rgb(167, 114, 125);
  // 해당 요소 내부의 텍스트 흰색으로 설정
  border-radius: 30px;
  // 해당 요소의 모서리를 30px 값으로 둥글게 처리
  cursor: grab;
  // 해당 요소에 마우스 커서를 올렸을 때, grab 커서를 표시하도록 설정
`;
// 버튼 클릭시 나오는 모달 창 구현
export const ModalView = styled.div.attrs((props) => ({
  // attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
  role: "dialog",
}))`
  // TODO : Modal창 CSS를 구현합니다.
  border-radius: 10px;
  background-color: #ffffff;
  width: 300px;
  height: 100px;
  margin-bottom: 200px;
  display: flex;
  flex-direction: column;
  // 해당 요소의 자식 요소를 수직(column) 방향으로 정렬
  align-items: center;
  > div.desc {
    font-size: 15px;
    color: rgb(233, 100, 121);
    font-weight: bold;
    // div.desc: 해당 요소의 자식 요소 중 클래스 이름이 desc인 요소 선택
  }
`;

export const Exitbtn = styled(ModalBtn)`
  background-color: #ffffff;
  color: black;
  margin: 10px;
  padding: 5px 10px;
  // 상하의 여백 지정
`;

export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);
  // useState Hook을 사용하여 isOpen과 setIsOpen 상태 변수를 정의
  // isOpen은 현재 모달이 열려있는지 여부를 나타내는 Boolean 값
  // setIsOpen은 isOpen 값을 변경하는 함수

  const openModalHandler = () => {
    // TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
    setIsOpen(!isOpen);
  };
  // 현재 isOpen 값을 반대로 변경하여 모달이 열리거나 닫힘 -> isOpen 값이 true이면 false로, false이면 true로 변경됨
  //  isOpen 상태 변수를 이용하여 모달이 열려있는지 여부를 체크 -> 모달의 열고 닫는 모달의 상태를 관리
  return (
    <>
      <ModalContainer>
        {/* ModalContainer -> 모달을 감싸는 컨테이너 역할 */}
        <ModalBtn
          // onClick 함수가 openModalHandler 함수를 호출하여 모달의 열리고 닫힘 상태를 변경
          // TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
          onClick={openModalHandler}
        >
          {isOpen ? "감사해영!" : "양말 한짝"}
          {/* 버튼의 라벨은 isOpen 변수를 이용하여 "Open Modal" 또는 "Opened!"로 표시 */}
          {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
        </ModalBtn>
        {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
        {isOpen ? (
          <ModalBackdrop onClick={openModalHandler}>
            {/* isOpen 변수를 이용하여 모달이 열려있는지 여부를 체크
             열려있는 경우 ModalBackdrop과 ModalView 컴포넌트가 렌더링
             배경을 클릭하면 onClick 함수가 호출되며, 이 함수는 openModalHandler 함수를 호출하여 모달을 닫음 */}
            {/* 자식요소는 이벤트 실행안되도록. '이벤트 버블링'이라는 현상을 막아줌*/}
            <ModalView
              onClick={(event) => {
                event.stopPropagation();
              }}
              // 배경 클릭 시 닫히지 않도록 stopPropagation() 함수를 사용하여 이벤트 버블링 현상을 막아줌
            >
              {/* x 버튼 */}
              <Exitbtn onClick={openModalHandler}>&times;</Exitbtn>
              <div className="desc">도비는 자유에옹!</div>
            </ModalView>
          </ModalBackdrop>
        ) : null}
      </ModalContainer>
    </>
  );
};

 

2-2. Toggle Component

 

 

import { useState } from "react";
import styled, { css } from "styled-components";

// 스타일드 컴포넌트를 사용하여 토글 스위치 스타일 정의
const ToggleContainer = styled.div`
  position: relative;
  margin-top: 8rem;
  left: 47%;
  // 상대 위치로 배치
  cursor: pointer;
  // 마우스 커서가 올라가면 손가락 모양의 커서로 표시

  > .toggle-container {
    // ToggleContainer 내부의 자식 요소 중 클래스 이름이 "toggle-container"인 요소를 선택
    width: 50px;
    height: 24px;
    border-radius: 30px;
    background-color: #8b8b8b;
    transition: 0.2s;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked {
      // &.toggle--checked는 .toggle-container에 toggle--checked 클래스가 추가된 경우에 대한 스타일을 지정
      // toggle--checked 클래스는 토글 스위치가 "on" 상태일 때 추가 -> background-color 속성 지정
      background-color: rgb(167, 114, 125);
      transition: 0.2s;
      // transition -> 스위치 상태 변화시 애니메이션
    }
  }
  // toggle switch의 원형 버튼 toggle-circle의 스타일링
  > .toggle-circle {
    position: absolute;
    // toggle-circle의 위치를 절대적으로 설정
    top: 1px;
    left: 1px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: #ffffff;

    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked {
      left: 27px;
      transition: 0.2s;
      // toggle-container와 toggle-circle 사이의 전환 효과
    }
  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  position: relative;
  top: 5px;
  left: 44%;
  /* color: ${(props) => (props.isChecked ? "rgb(233, 100, 121)" : "black")}; */
  /* color: ${({ isChecked }) =>
    isChecked ? "rgb(167, 114, 125)" : "black"}; */

  /* ${(props) => {
    if (props.isChecked)
      return css`
        color: rgb(167, 114, 125);
        font-weight: bold;
      `;
  }}*/

  ${(props) =>
    props.isChecked &&
    css`
      color: rgb(167, 114, 125);
      font-weight: bold;
    `}/* &.toggle--checked {
    color: red;
  } */
`;

export const Toggle = () => {
  const [isOn, setisOn] = useState(false);

  const toggleHandler = () => {
    // TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
    setisOn(!isOn);
  };

  return (
    <>
      {/* 리액트 컴포넌트로 토글 스위치 구현 */}
      <ToggleContainer
        // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        onClick={toggleHandler}
      >
        {/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
        {/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
        <div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
        <div className={`toggle-circle ${isOn ? "toggle--checked" : ""}`} />
      </ToggleContainer>
      {/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
      {/* TIP:  Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
      {/* <Desc className={isOn ? "toggle--checked" : ""}> */}
      <Desc isChecked={isOn}>
        {" "}
        {isOn ? "이제 일해라 닝겐!" : "미이야~아~옹!"}
      </Desc>
      {/* Desc 컴포넌트로 토글 스위치의 상태를 나타내는 텍스트를 렌더링
      스위치가 켜진 상태이면 "Toggle Switch ON", 꺼진 상태이면 "Toggle Switch OFF */}
    </>
  );
};

 

2-3. Tab Component

 

import { useState } from "react";
import styled, { css } from "styled-components";

// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.

const TabMenu = styled.ul`
  background-color: rgb(249, 245, 231);
  color: rgba(73, 73, 73, 0.5);
  font-weight: bold;
  display: flex;
  flex-direction: row;
  justify-items: center;
  align-items: center;
  list-style: none;
  margin-bottom: 7rem;
  padding: 0;

  .submenu {
    ${"" /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
    background-color: rgb(249, 245, 231);
    width: calc(100% / 3);
    height: 30px;
    text-align: center;
    transition: 0.5s;
    padding: 10px;
  }

  .focused {
    ${"" /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */}
    //background-color: var(--coz-purple-600);
    background-color: rgb(167, 114, 125);
    color: white;
    width: calc(100% / 3);
    height: 30px;
    text-align: center;
    transition: 0.5s;
  }
`;

const Desc = styled.div`
  text-align: center;
  /* ${(props) =>
    props.isChecked &&
    css`
      color: rgb(167, 114, 125);
      font-weight: bold;
    `} */
`;

export const Tab = () => {
  // TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
  // currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.
  const [focusedTab, setFocusedTab] = useState(0);

  const menuArr = [
    {
      name: "상견니",
      content: "너라면, 나를 절대 잊지 않을거고, 결국엔 나를 찾을거라고 믿어!",
    },
    {
      name: "깨비깨비도깨비",
      content:
        "너와 함께한 시간 모두 눈부셨다. 날이 좋아서, 날이 좋지 않아서, 날이 적당해서...",
    },
    { name: "한산", content: "지금 우리에겐 압도적인 승리가 필요하다!" },
  ];

  const selectMenuHandler = (index) => {
    // TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
    // TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
    setFocusedTab(index);
  };

  return (
    <>
      <div>
        <TabMenu>
          {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                  나머지 2개의 tab은 'submenu' 가 됩니다.*/}
          {/* 핸들러 함수 연결 해줄 때 함수 실행 형태로 넣으면 안됨! 콜백함수 형태로 넣어야함! 
              selectMenuHandler(index) 이렇게만 넣으면 실행 형태가 됨*/}
          {menuArr.map((el, index) => {
            return (
              <li
                key={index}
                className={`submenu${focusedTab === index ? " focused" : ""}`}
                onClick={() => selectMenuHandler(index)}
              >
                {el.name}
              </li>
            );
          })}
        </TabMenu>
        <Desc isChecked={focusedTab}>
          {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
          <p>{menuArr[focusedTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

 

2-4. Tag Component

import { useState } from "react";
import styled from "styled-components";

// TODO: Styled-Component 라이브러리를 활용해 여러분만의 tag 를 자유롭게 꾸며 보세요!

export const TagsInput = styled.div`
  margin: 8rem auto;
  // 위, 아래 여백을 8rem, 좌우 여백을 자동으로 설정
  display: flex;
  // flex박스를 사용하여 요소를 배치
  align-items: flex-start;
  // 주축을 따라 아이템을 위쪽으로 정렬
  flex-wrap: wrap;
  // flex박스에서 아이템이 한 줄을 넘어갈 때 줄을 바꿈
  min-height: 48px;
  // 최소 높이를 48px로 설정
  width: 480px;
  padding: 0 8px;
  // 위아래 여백을 0, 좌우 여백을 8px로 설정
  border: 3px solid rgb(167, 114, 125);
  // 테두리를 3px의 굵기
  border-radius: 6px;

  > ul {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    margin: 8px 0 0 0;

    > .tag {
      width: auto;
      height: 32px;
      display: flex;
      align-items: center;
      // 세로 방향 정렬을 가운데로 설정
      justify-content: center;
      // 가로 방향 정렬을 가운데로 설정
      color: rgb(167, 114, 125);
      padding: 0 8px;
      font-size: 14px;
      list-style: none;
      border-radius: 6px;
      margin: 0 8px 8px 0;
      // background: var(--coz-purple-600);
      background: rgb(248, 234, 216);

      > .tag-close-icon {
        display: block;
        width: 16px;
        height: 16px;
        line-height: 16px;
        text-align: center;
        font-size: 14px;
        margin-left: 8px;
        // color: var(--coz-purple-600);
        color: rgb(167, 114, 125);
        border-radius: 50%;
        background: rgb(249, 245, 231);
        cursor: pointer;
      }
    }
  }

  > input {
    flex: 1;
    // input 요소가 부모 요소의 남은 공간을 모두 차지할 수 있도록 설정
    border: none;
    height: 46px;
    font-size: 14px;
    padding: 4px 0 0 0;
    :focus {
      outline: transparent;
      // input 요소가 포커스될 때 outline을 투명하게 처리합니다.
    }
  }

  &:focus-within {
    border: 1px solid rgb(167, 114, 125);
    // var(--coz-purple-600);

    // 해당 요소 또는 해당 요소의 자식 요소 중 하나가 포커스될 때, 요소에 1px의 실선 테두리의 색상을 설정
  }
`;

export const Tag = () => {
  const initialTags = ["일상그램", "도비는가끔눈물을흘린다"];

  const [tags, setTags] = useState(initialTags);
  //useState 훅을 사용 -> tags 배열과 tags 배열을 변경하는 함수 setTags를 선언
  const removeTags = (indexToRemove) => {
    // 인덱스(indexToRemove)를 전달받아 해당 인덱스의 태그를 제거
    // TODO : 태그를 삭제하는 메소드를 완성하세요.
    let newTags = tags.slice();
    // tags 배열을 복사한 새로운 배열을 newTags 변수에 할당
    newTags.splice(indexToRemove, 1);
    // newTags 배열에서 indexToRemove 인덱스의 요소를 1개 제거
    setTags(newTags);
    // 변경된 newTags 배열을 setTags 함수를 사용하여 tags 배열에 저장
  };
  // addTags 함수 -> 이벤트(event) 객체를 전달받아 새로운 태그를 추가하는 역할
  const addTags = (event) => {
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
    let newTag = event.target.value.trim();
    if (
      !tags.includes(newTag) &&
      newTag.length !== 0 &&
      event.key === "Enter"
      // 태그를 추가할 수 있도록 if문을 사용하여 조건을 검사 (3가지)
      // -> 새로운 태그가 이전에 추가된 적이 있는지 검사하여 이미 있는 태그면 추가X
      // -> 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행X
      // -> 태그가 추가되면 input 창 비워주기
    ) {
      let newTags = tags.slice();
      // tags 배열을 복사한 새로운 배열을 newTags 변수에 할당
      newTags.push(newTag);
      // newTag를 newTags 배열에 추가
      // setTags 함수로 변경된 newTags 배열 tags 배열에 저장 -> 새로운 태그 추가 완료
      setTags(newTags);
      event.target.value = "";
      // 이벤트의 대상(target) 요소의 값을 빈 문자열("")로 초기화 -> 입력 필드 비워두기
    }
  };

  return (
    <>
      <TagsInput>
        <ul id="tags">
          {tags.map((tag, index) => (
            <li key={index} className="tag">
              <span className="tag-title">{tag}</span>
              <span
                className="tag-close-icon"
                onClick={() => removeTags(index)}
              >
                {/* TODO :  tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
                            삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
                x
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={addTags}
          // input 요소에서 키보드가 눌렸을 때
          // 발생하는 이벤트(onKeyUp)에 입력된 값을 검증하여 태그 목록에 새로운 태그를 추가하는 addTags 함수를 연결시킴
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

'CodeStates > 블로깅 챌린지' 카테고리의 다른 글

[React] 상태 관리  (0) 2023.02.23
Create React App을 이용해서 리액트 설치하기  (0) 2023.02.23
React 가상돔  (0) 2023.02.21
React 오리엔테이션  (0) 2023.02.20
UI/UX  (0) 2023.02.17