팀프로젝트: Wingle(2.2) 공통UI 제작 - 드롭다운 컴포넌트

윤뿔소·2023년 5월 15일
1

팀프로젝트: Wingle

목록 보기
15/16

전 글에는 드롭다운의 로직을 짜봤다. 이제 만든 Props를 가지고 연결해보자.

모델 다시 보기: interface

전편에 만들었던 드롭다운 Props interface를 다시 보자.

interface DropDownProps {
  label?: string; // 제목
  list: string[]; // 드롭다운 리스트
  selected: string; // 선택된 항목(selected state)
  dropDownPlaceHolder?: string; // 드롭다운 플레이스홀더
  handleSelectedChange: (selected: string) => void; // 선택된 항목 변경 함수(selected setState 변경 함수)
  description?: string; // 드롭다운 설명
  disabled?: boolean; // 드롭다운 비활성화
}
export default function DropDownCommon({
  label,
  list,
  selected,
  dropDownPlaceHolder,
  handleSelectedChange,
  description,
  disabled = false,
}: DropDownProps)

위와 같다. 이제 클릭할 때 드롭다운의 내용이 변하고, disabled, 제목 등의 디자인을 해보자.

기능 구현

상태 getter / setter 함수 연결하기

전에 text.tsx에 썼던 const [selected, setSelected] = useState<string>(""); 문장을 썼고 그를 Props로 내려줬다.
값을 바꾸기위해 setter인 handleSelectedChangesetSelected를 받아주도록 연결하고, getter인 selected를 값으로 연결해 받아줄 것이다.

  const toggleDropdown = () => {
    setIsActive(!isActive);
  };

  const handleSelect = (item: string) => {
    handleSelectedChange(item);
    setIsActive(false);
  };

  return (
    <S.Container>
      {label && <S.DropDownLabel disabled={disabled}>{label}</S.DropDownLabel>}
      <S.DropdownContainer>
        <S.DropdownBody
          onClick={toggleDropdown}
          isActive={isActive}
        >
          <S.DropdownSelected>
            {selected || dropDownPlaceHolder || "Select an item"}
          </S.DropdownSelected>
          <S.DropdownSelected>
            <Image
              src="/auth/arrow_down.svg"
              alt="arrow"
              width={20}
              height={20}
            />
          </S.DropdownSelected>
        </S.DropdownBody>
        
        <S.DropdownMenuContainer isActive={isActive}>
          {list.map((item) => (
            <S.DropdownItemContainer
              key={item}
              onClick={() => {
                handleSelect(item);
              }}
            >
              {item}
            </S.DropdownItemContainer>
          ))}
        </S.DropdownMenuContainer>
      </S.DropdownContainer>
      {description && <S.Description>{description}</S.Description>}
    </S.Container>
  );
...
  1. handleSelecthandleSelectedChange 작성
    • handleSelectedChange는 setter함수
    • item을 파라미터로 받음. itemDropdownMenuContainerlist에서 고른 string
  2. DropdownSelected 추가
    • selected를 받아줘서 있으면 렌더링
    • selected가 없다면 Placeholder, 없으면 기본값 렌더링
    • 아래 화살표 아이콘 추가
  3. DropdownItemContainerhandleSelect(item); 넣음
    • onClick할 때 item 값을 받고 setter에 전해주기
  4. label, description : TextInputUI 만들 때 처럼 넣어주기

이런 식으로 넣었다. 이렇게 된다면 listonClick으로 발생된 이벤트 handleSelect(item);가 실행되고, handleSelectedChange(item);가 실행돼서 바뀔 것이다!

처음에 이거 하는데 로직이 좀 헷갈려서 30분을 헤맸다.. 항상 상태 getter, setter를 같이 사용해주도록 생각하자.

디자인

노가다의 꽃 디자인이다. 드롭다운 디자인을 다시 보자!

  1. 선택 전, 선택 중, 선택 후
  2. disabled
  3. Hover 및 선택된 드롭다운 항목은 배경이 달라짐

이 3가지 상태가 필요하다. 그래서 disabled, selected, isActive를 비교해서 디자인을 해야한다.

일단 기본적인 디자인은 아래와 같다.

const S = {
  Container: styled.div`
    display: flex;
    flex-direction: column;
  `,
  DropDownLabel: styled.label<{ disabled: boolean }>`
    margin-bottom: 8px;
    font-size: 16px;
    font-weight: 700;
    color: ${({ theme, disabled }) =>
      disabled ? theme.color.gray500 : theme.color.gray700};
  `,
  DropdownContainer: styled.div`
    position: relative;
    &:hover {
      cursor: pointer;
    }
  `,
  DropdownBody: styled.div`
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 452px;
    height: 50px;
    background-color: ${({ theme }) =>
      theme.color.white};
    border: 1px solid
      ${({ theme }) =>
        theme.color.gray600};
    border-radius: 8px;
  `,
  DropdownSelected: styled.div`
    font-size: 16px;
    color: ${({ theme }) =>
  	  theme.color.gray900
    };
    padding: 14px 16px;
  `,
  DropdownMenuContainer: styled.ul<{ isActive: boolean }>`
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    z-index: 1;
    width: 452px;
    height: 312px;
    padding: 8px 0;
    background-color: ${({ theme }) => theme.color.white};
    border: 1px solid ${({ theme }) => theme.color.gray600};
    border-radius: 8px;
    box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.1);
    overflow-y: auto;
    display: ${(props) => (props.isActive ? "block" : "none")};
  `,
  DropdownItemContainer: styled.li`
    display: flex;
    align-items: center;
    padding: 16px;
    font-size: 16px;
    color: ${({ theme }) => theme.color.gray900};
    &:hover {
      background-color: ${({ theme }) => theme.color.gray200};
      cursor: pointer;
    }
  `,
  Description: styled.div`
    height: 17px;
    font-size: 12px;
    color: ${({ theme }) => theme.color.gray900};
    flex: none;
    order: 2;
    align-self: stretch;
    flex-grow: 0;
    margin: 8px 0px;
  `,
};

전중후 디자인

일단 만들면서 disabled 디자인도 같이 적용하겠다. 선택 전 선택 중 선택 후 디자인 차이를 만들어보자.

  1. 선택 전 : 선택 전에는 기본적인 border와 회색 폰트 색깔
  2. 선택 중 : 선택 중에는 진한 border
  3. 선택 후 : 선택 후 선택 전으로 border가 돌아가고 진한 폰트 색깔

이런 식이다. 그걸 만들어보자.

  DropdownBody: styled.div<{ disabled: boolean; isActive: boolean }>`
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 452px;
    height: 50px;
    background-color: ${({ theme, disabled }) =>
      disabled ? theme.color.gray200 : theme.color.white};
    border: 1px solid
      ${({ theme, isActive }) =>
        isActive ? theme.color.gray600 : theme.color.gray200};
    cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")};
    border-radius: 8px;
  `,
  DropdownSelected: styled.div<{ disabled: boolean; selected: string }>`
    font-size: 16px;
    color: ${({ theme, disabled, selected }) =>
      disabled
        ? theme.color.gray500
        : selected
        ? theme.color.gray900
        : theme.color.gray500};
    padding: 14px 16px;
  `,

isActive로 일단 드롭다운 기본 박스인 DropdownBody의 border를 조작했다. 그 다음 그 안 내용의 디자인을 정하는 DropdownSelectedselected가 있다면 gray900, 없다면 흐린 폰트 색깔인 gray500를 만들어 놨다.

거기에 disabled가 된다면 클릭하지 못한다는 UX를 주기 위해서 배경: gray200, 폰트색: gray500으로 설정했다. 이중 삼항연산자라 읽기가 불편하긴 한데 Styled-components Props 특성 상 어쩔 수 없다.

선택된 항목, Hover 배경 디자인 적용하기

  return (
    ...
        <S.DropdownMenuContainer isActive={isActive}>
          {list.map((item) => (
            <S.DropdownItemContainer
              key={item}
              isSelected={item === selected}
              onClick={() => {
                handleSelect(item);
              }}
            >
              {item}
            </S.DropdownItemContainer>
          ))}
        </S.DropdownMenuContainer>
    ...
const S = {
    ...
  DropdownItemContainer: styled.li<{ isSelected: boolean }>`
    display: flex;
    align-items: center;
    padding: 16px;
    font-size: 16px;
    color: ${({ theme }) => theme.color.gray900};
    &:hover {
      background-color: ${({ theme }) => theme.color.gray200};
      cursor: pointer;
    }
    ${({ isSelected, theme }) =>
      isSelected &&
      `
    background-color: ${theme.color.gray200};
  `}
  `,
    ...
  1. 선택된 항목
    a. boolean을 받는 isSelected Props를 만들어서 itemselected를 일치 연산자로 하여금 일치되는 리스트가 있는지 판단
    b. isSelected&& 연산자로 true라면 gray200으로 배경색 적용하기
  2. Hover : 아주 쉽게 gray200으로 hover 적용

이렇게 하면 선택된 항목이 있으면 드롭다운 컨테이너에 나타나게 된다! 길고 긴 드롭다운 디자인 기능 만들기 성공이다.

결과

일단 먼저 test.tsx 을 만들어 적용해보자.

import { useState } from "react";
import { Margin } from "../components/ui";
import DropDown from "../components/ui/dropDownUI";
import styled from "styled-components";

export default function Test() {
  const [selected, setSelected] = useState<string>("");
  // const [isButtonDisabled, setButtonDisabled] = useState<boolean>(true);
  const handleSelectedChange = (selected: string) => {
    setSelected(selected);
  };

  const countryList = [
    {
      code: "KR",
      nation: "대한민국",
      add_code_nation: "KR 대한민국",
      country: "REPUBLIC OF KOREA",
    },
    ...
  ];

  return (
    <S.Wrapper>
      <Margin size={50} direction="column" />
      <div>
        <Margin size={8} direction="row" />
        <DropDown
          label="안녕"
          list={countryList.map((item) => item.country)}
          selected={selected}
          dropDownPlaceHolder="선택하세요"
          // disabled={isButtonDisabled}
          handleSelectedChange={handleSelectedChange}
          description="테스트"
        />
      </div>
    </S.Wrapper>
  );
}

disabled 적용

드롭다운을 클릭할 수 없게 됐다!

드롭다운 결과 보기

짠! 잘됐다!


이건 좀 노가다성이 좀 크고 어디부터 어디까지 공통 UI로 만들어서 해야할지 고민이었다. 객체 데이터를 주로 받아서 하기 때문에 객체 자체를 받을 수 없어 어떻게 받아줄지도 고민이었다.

그래서 고민 끝에 string[]으로 이루어진 데이터만 받게 기획했고, 그에 따라 술술 풀려갔다.

다음엔 상태의 getter와 상태의 setter를 받고 어디 배치할지 어떻게 순서가 이루어질지도 잘 생각해서 해봐야겠다. 은근 삽질을 많이 했다.

profile
코뿔소처럼 저돌적으로

4개의 댓글

comment-user-thumbnail
2023년 5월 15일

고생하셨습니다 드롭다운이 확실히 복잡하네요 ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 5월 21일

고생하셨습니다 !

답글 달기
comment-user-thumbnail
2023년 5월 21일

저는 select 태그로 대충 만들었는데, 고생하신 만큼 너무 잘 만드셨습니당 ㅎㅎ

답글 달기
comment-user-thumbnail
2023년 5월 21일

상태가 3가지나 필요한 드롭다운이라니.. 고생하셨습니다..!!!

답글 달기