드림오구
article thumbnail

🌊 React Custom Component 과제

오늘은 리액트 hook을 사용하여 Custom Component 과제를 하였습니다. 

Bare minimum Requirement로 Modal, Toggle, Tab, Tag를 구현 과제가 나왔고 Advanced Challenge는 내일 도전해볼 생각입니다.

오늘 구현한 Modal, Toggle, Tab, Tag를 소개하고 코드 작성시 고려했던 점을 리뷰하려 합니다.

 

React Custom Component

Modal

export const ModalContainer = styled.div`
  // TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.3);
`;

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;
`;

export const ModalView = styled.div.attrs((props) => ({
  // attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
  role: 'dialog',
}))`
  // TODO : Modal창 CSS를 구현합니다.
  width: 400px;
  height: 200px;
  border-radius: 1rem;
  padding: 1rem;
  background: #fff;
  position: relative;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
`;

export const CloseBtn = styled.button`
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: var(--coz-purple-600);
  color: #fff;
  width: 30px;
  height: 30px;
  font-size: 1.5rem;
  line-height: 30px;
  border-radius: 100%;
  cursor: pointer;
`;
export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModalHandler = (event) => {
    setIsOpen(!isOpen); // 부정연산자로 반대로 변경해준다.
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn onClick={openModalHandler}>
          {isOpen ? 'Opened!' : 'Open Modal'}
        </ModalBtn>
        {isOpen && (
          <ModalBackdrop onClick={openModalHandler}>
            <ModalView onClick={(event) => event.stopPropagation()}>
              <CloseBtn onClick={openModalHandler}>X</CloseBtn>
              <div>Hello CODESTATES!</div>
            </ModalView>
          </ModalBackdrop>
        )}
      </ModalContainer>
    </>
  );
};

사실 조건을 많이 써주셔서 Bare Minimum 단계는 생각보단 쉽게 진행을 하였습니다. 

Modal 구현

 

신경쓴 점

  • isOpen일 때 Modal이 출력되게 하는 것은 단축 평가 논리 연산자를 사용했습니다. 잘 쓸 수 있을 지 늘 고민하던 방법이라 바로 적용해보았습니다.
  • 부트캠프에서 미리 삼항연산자 사용을 제시하였지만 요즘 더욱 더 잘 쓰고 있는 것 같습니다.
  • ModalView가 중앙에 올 수 있게 하기 위해 position 지정 후, left: 50%;  top: 50%; transform: translate(-50%,-50%)을 사용하였습니다.
  • 기존 Close-btn의 모양이 버튼처럼 보이게 하고 싶어 배경색을 주었습니다. 

 

Toggle

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;
    position: relative;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked {
      background: var(--coz-purple-600);
      transition: 1s;
    }
    > .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: auto;
        right: 1px;
        transition: 3s;
      }
    }
  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  display: flex;
  justify-content: center;
  margin: 1rem;
`;
export const Toggle = () => {
  const [isOn, setisOn] = useState(false);

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

  return (
    <>
      <ToggleContainer
        onClick={toggleHandler}
        // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
      >
        {/* 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' : ''}`} />
        </div>
      </ToggleContainer>
      <Desc>Toggle Swith {isOn ? 'On!!' : 'Off'}</Desc>
      {/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
      {/* TIP:  Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
    </>
  );
};

별건 아니지만 기존에 주어진 코드는 toggle-container와 toggle-circle이 따로 있었는데 저는 container 안에 circle을 넣어보았습니다. 이유는 지금은 Toggle 하나만 구현하는 거지만 이것이 반응형에서 사용하게 된다면 모양이나 크기가 달라질 수 있으므로 left: *px을 주는 것보단 right: 1px을 주는 것이 코드의 효율을 높인다고 생각했습니다. 그래서 부모인 container가 기준이 되게 하기 위해 position: relative을 주고 light: 1px을 주었습니다.

Toggle

 

 

신경쓴 점

  • 위에 적은 toggle-container와 toggle-circle의 css

 

Tab

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;
  width: 90%;
  margin: auto auto 4rem;

  .submenu {
    ${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
    flex: 1;
    padding: 1rem 0;
    text-align: center;
  }

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

  & div.desc {
    text-align: center;
  }
`;
export const Tab = () => {
  // TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
  // currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.
  const [currentTab, setCurrentTab] = 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) => {
    setCurrentTab(index);
  };

  return (
    <>
      <div>
        <TabMenu>
          {menuArr.map((el, index) => {
            return (
              <li
                key={index}
                className={`submenu${currentTab === index ? ' focused' : ''}`}
                onClick={() => selectMenuHandler(index)}
                // onclick에 함수 자체를 넣으면 안된다.
              >
                {el.name}
              </li>
            );
          })}
        </TabMenu>
        <Desc>
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

Tab을 구현할 때 onClick에 함수명 자체를 넣는 실수를 하였습니다. 지금은 무사히 수정 완료! 

 

Tab

 

신경 쓴 점

  • currenetTab이 내가 클릭하고 있는 현재 탭의 인덱스이니 Desc 컴포넌트 안에 menuArr[currentTab].content로 내용을 불러왔습니다. 

 

 

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;
      }
      > .tag-title {
        color: #fff;
      }
    }
  }

  > input {
    flex: 1;
    border: none;
    height: 40px;
    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((tag) => {
        return tag !== tags[indexToRemove];
      })
    );
  };

  const addTags = (event) => {
    let value = event.target.value;
    if (event.key === 'Enter' && !tags.includes(value) && value) {
      // 이벤트 키가 엔터거나, tags에 없거나, value가 있을 때
      setTags([...tags, value]);
      event.target.value = '';
    } else if (event.key === 'Enter' && !value) {
      // 이벤트 키가 엔터거나, value가 없을 때
      event.target.value = '';
    }
  };

  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)}
              >
                X
              </span>
            </li>
          ))}
        </ul>
        <input
          className='tag-input'
          type='text'
          onKeyUp={(event) => {
            addTags(event);
          }}
          placeholder='Press enter to add tags'
        />
      </TagsInput>
    </>
  );
};

조건 때문에 논리연산자를 사용하였습니다. 

  • 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
  • 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
  • 태그가 추가되면 input 창 비우기

Tag

 

신경 쓴 점

  • event.target.value 사용하기

 

 

 

느낀점

저는 CSS를 자주 다뤄봐서 쉽게 해결한 문제가 있었습니다. 페어님께서는 CSS로 모달 구현을 하다 시간을 많이 뺏기신 것 같아 함께 얘기하며 문제를 해결하는 소중한 시간이었습니다. 사실 리액트 부분은 힌트를 많이 주셔서 수월하게 풀 수 있었던 것 같습니다.

 

profile

드림오구

@드림오구