티스토리 뷰

코드스테이츠

TIL 23.02.21

_Yunhwan 2023. 2. 21. 18:03

오늘은 React로 자주 사용하는 UI 컴포넌트의 기능 구현을 해보는 시간을 가졌다.

 

Modal, Toggle, Tab, Tag 의 각각의 코드와 설명을 적어보려고 한다.

 

 

Modal

 

 

코드

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

export const ModalContainer = styled.div`
  // TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
  width: 100%;
  height: 100%;
  position: relative;
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
  background-color: rgb(0, 0, 0, 0.5);
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
`;

export const ModalBtn = styled.button`
  background-color: var(--coz-purple-600);
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
`;

export const ModalView = styled.div.attrs((props) => ({
  // attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
  role: "dialog",
}))`
  // TODO : Modal창 CSS를 구현합니다.
  background-color: white;
  position: ablsolute;
  width: 50%;
  height: 50%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
`;

export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

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

  return (
    <>
      <ModalContainer>
        {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
        {isOpen ? (
          <>
            <ModalBtn
              // TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
              onClick={openModalHandler}
            >
              {isOpen ? "Opened" : "Open Modal"}
              {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
            </ModalBtn>
            <ModalBackdrop onClick={openModalHandler}>
              <ModalView onClick={(event) => event.stopPropagation()}>
                <div>
                  <p>Hello</p>
                </div>
                <div>
                  <button onClick={openModalHandler}>Close</button>
                </div>
              </ModalView>
            </ModalBackdrop>
          </>
        ) : (
          <ModalBtn
            // TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
            onClick={openModalHandler}
          >
            {isOpen ? "Opened" : "Open Modal"}
            {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
          </ModalBtn>
        )}
      </ModalContainer>
    </>
  );
};

 

가장 간단해 보이지만, 만드는 데 가장 많이 시간이 걸렸던 Modal 화면이다.

기존에는 CSS의 display: absolute 속성을 이용하여서, 화면 전체에 모달의 배경을 주었지만,

위 예제에서는 div 요소안에만 적용시켜야 했기 때문에, 여러 실패를 겪으면서 해결법을 찾았다.

 

바로 모달을 넣고 싶은 상위 요소에 display: relative 속성을 준 뒤, 모달 배경에 대해서 display: absolute 속성을 주어

상위 요소를 기준점으로 잡아서 모달 배경을 위치시켰다. 

 

그 후, 모달 배경 내부에 모달 창을 생성하였고,  open 버튼, 모달 배경화면, close 버튼에 state를 변경해주는 이벤트를 부착해 

기능을 구현하였다.

 

하지만 모달 창을 눌렀을 때도, state가 변경되어 모달이 닫히는 상황이 생겼는데,

그 이유는 이벤트가 하위컴포넌트까지 전파가 되었기 때문이다.

따라서 이벤트의 전파를 막기위해서 아래와 같이 작성해 준 뒤, 이벤트 전파를 막는 것을 확인했다.

 <ModalView onClick={(event) => event.stopPropagation()}>

 

 

Toggle

 

코드

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

const ToggleContainer = styled.div`
  position: relative;
  margin-top: 8rem;
  left: 47%;
  cursor: pointer;

  > .toggle-container {
    width: 50px;
    height: 24px;
    border-radius: 30px;
    background-color: #8b8b8b;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked {
      background-color: var(--coz-purple-600);
    }
  }

  > .toggle-circle {
    position: absolute;
    top: 1px;

    left: 1px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: #ffffff;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked {
      left: 27px;
    }
  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.

  display: flex;
  justify-content: center;
  align-items: center;
  margin: 10px;
`;

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

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

  return (
    <>
      <ToggleContainer
        // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        onClick={toggleHandler}
      >
        {/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
        {/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}

        {isOn ? (
          <>
            <div className="toggle-container toggle--checked" />
            <div className="toggle-circle toggle--checked" />
          </>
        ) : (
          <>
            <div className="toggle-container" />
            <div className="toggle-circle" />
          </>
        )}
      </ToggleContainer>
      {/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
      {/* TIP:  Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
      <Desc>
        {isOn ? <span>Toggle Switch ON</span> : <span>Toggle Switch OFF</span>}
      </Desc>
    </>
  );
};

 

토글은 비교적 쉽게 진행하였다.

클릭 시 state를 변경해주면서 class또한 변경해주는 식으로 구현하였다.

 

styled components가 익숙치 않아 toggle-checked 클래스가 있을경우 style을 추가하는 부분에서 어려움을 겪었다.

 

구글링을 통해서 찾아보다가, 현재의 요소를 뜻하는 &(ampersand) 를 통해서 문제를 해결하였다.

&를 통해 해당 컴포넌트를 참조하고 .toggle-checked 에 스타일을 적용하였더니 쉽게 구현할 수 있었다.

 

 

Tab

 

코드

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

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

const TabMenu = styled.ul`
  background-color: #dcdcdc;
  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;

  .submenu {
    ${"" /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
    display: flex;
    flex: 1;
  }

  .focused {
    ${"" /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */}
    background-color: var(--coz-purple-600);
    color: white;
    display: flex;
  }

  & div.desc {
    text-align: center;
  }
`;

const Desc = styled.div`
  text-align: center;
`;

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

  const menuArr = [
    { name: "Tab1", content: "Tab menu ONE" },
    { name: "Tab2", content: "Tab menu TWO" },
    { name: "Tab3", content: "Tab menu THREE" },
  ];

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

  return (
    <>
      <div>
        <TabMenu>
          {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                  나머지 2개의 tab은 'submenu' 가 됩니다.*/}
          {menuArr.map((it, index) =>
            index === currentTab ? (
              <li
                className="submenu focused"
                onClick={() => selectMenuHandler(index)}
                key={index}
              >
                {it.name}
              </li>
            ) : (
              <li
                className="submenu"
                onClick={() => selectMenuHandler(index)}
                key={index}
              >
                {it.name}
              </li>
            )
          )}
        </TabMenu>
        <Desc>
          {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

 

탭은 우선 배열로된 요소들을 map 메서드를 통해 나열 시킨 뒤

currentTab 상태와 인덱스가 일치하는 경우에만 focus를 주는 li 요소들을 만들었고,

각 li요소에는 클릭된 탭의 인덱스를 currentTab으로 변경시켜 주는 이벤트를 부착해주어 구현을 하였다.

 

 

Tag

 

코드

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

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

export const TagsInput = styled.div`
  margin: 8rem auto;
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  min-height: 48px;
  width: 480px;
  padding: 0 8px;
  border: 1px solid rgb(214, 216, 218);
  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: #fff;
      padding: 0 8px;
      font-size: 14px;
      list-style: none;
      border-radius: 6px;
      margin: 0 8px 8px 0;
      background: var(--coz-purple-600);
      > .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);
        border-radius: 50%;
        background: #fff;
        cursor: pointer;
      }
    }
  }

  > input {
    flex: 1;
    border: none;
    height: 46px;
    font-size: 14px;
    padding: 4px 0 0 0;
    :focus {
      outline: transparent;
    }
  }

  &:focus-within {
    border: 1px solid var(--coz-purple-600);
  }
`;

export const Tag = () => {
  const initialTags = ["CodeStates", "kimcoding"];

  const [tags, setTags] = useState(initialTags);
  const removeTags = (indexToRemove) => {
    // TODO : 태그를 삭제하는 메소드를 완성하세요.

    setTags(tags.filter((it, index) => index !== indexToRemove));
  };

  const addTags = (event) => {
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
    // 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
    // - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    // - 태그가 추가되면 input 창 비우기
    const tag = event.target.value;
    if (tags.includes(tag) || tag.length === 0) {
      console.log("Fail!");
      return;
    }
    event.target.value = "";
    setTags([...tags, tag]);
  };

  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={(e) => {
            {
              /* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */
              if (e.key === "Enter") {
                addTags(e);
              }
            }
          }}
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

 

Tag는 전형적인 create,delete의 형태와 유사한 작업이었다.

tags 배열을 state로 관리하고 addTag, removeTags 함수를 이용하여 태그를 추가하고 삭제하는 작업을 구현하였는데,

 

tags를 map메서드로 나열 한뒤, x를 클릭 시 removeTags를 실행하는 이벤트를 부착해주었다.

removeTags는 tags에서 현재 선택된 index를 가진 요소만 filter로 제외시킨 배열로 tags의 state를 변경해주어 구현하였다.

 

input 창에서는 key가 enter일 때, addTag 를 실행하는 이벤트를 발생시켜

기존에 없는 태그이며, 빈 칸이 아닐 때  tags에서 [...tags,tag]로 tags의 state를 변경해주어 구현하였다.

 

 

'코드스테이츠' 카테고리의 다른 글

TIL 23.02.23  (0) 2023.02.23
TIL 23.02.22  (0) 2023.02.22
TIL 23.02.20  (0) 2023.02.20
TIL 23.02.16  (0) 2023.02.16
TIL 23.02.15  (0) 2023.02.15
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함