원티드 프리온보딩 프론트엔드 1차개인과제

gyujae·2022년 5월 8일
0
post-custom-banner


원티드 프론트엔드 선발 과제 리팩토링 하기

Github: https://github.com/GyuJae/wanted_pre_onboarding_refatoring
URL: https://gyujae.github.io/wanted_pre_onboarding_refatoring/

  1. jsx + style.module.css 형식으로 리팩토링하기
  2. eslint 설정으로 리팩토링하기
  3. 1일차 이론시간에 배운 리팩토링 방식 사용하기 ex) handle function 으로 event 함수 정의 하기 등...

Toggle

before refactoring

import React, { useState } from "react";
import { cls } from "../libs/utils";

interface IItem {
 select: boolean;
 text: string;
 setSelect: () => void;
}

const Item: React.FC<IItem> = ({ select, text, setSelect }) => {
 return (
   <div
     className={cls(
       "z-10 cursor-pointer font-extrabold transition ease-in-out delay-100 w-full h-full rounded-full flex justify-center items-center",
       select ? "" : "text-gray-400"
     )}
     onClick={setSelect}
   >
     {text}
   </div>
 );
};

const Toggle = () => {
 const [detail, setDetail] = useState<boolean>(false);

 const onStandClick = () => setDetail(false);

 const onDetailClick = () => setDetail(true);

 return (
   <div>
     <div className="relative w-96 bg-gray-200 h-14 rounded-full flex justify-around items-center">
       <Item setSelect={onStandClick} select={!detail} text={"기본"} />
       <Item setSelect={onDetailClick} select={detail} text={"상세"} />
       <div
         className={cls(
           "transition ease-in-out delay-150 absolute w-44 bg-white h-5/6 rounded-full",
           detail ? "translate-x-24" : "-translate-x-24"
         )}
       />
     </div>
   </div>
 );
};

export default Toggle;

after refatoring

import { useState } from 'react'
import { cx } from '../../styles'
import styles from './Toggle.module.scss'

const data = ['기본', '상세']

const Toggle = () => {
 const [dataItem, setDataItem] = useState<string>(data[0])

 const handleClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
   const {
     currentTarget: { innerText },
   } = e
   setDataItem(innerText)
 }

 return (
   <div className={styles.container}>
     {data.map((item) => (
       <button
         key={item}
         className={cx(styles.item, { [styles.selected]: dataItem === item })}
         type='button'
         onClick={handleClick}
       >
         {item}
       </button>
     ))}
     <div className={cx(styles.box, { [styles.xEnd]: dataItem === data[1] })} />
   </div>
 )
}

export default Toggle
@use '/src/styles/constants/colors';
@use '/src/styles/mixins/flexbox';

.container {
 position: relative;
 display: flex;
 gap: 2px;
 justify-content: space-between;
 width: 400px;
 background-color: colors.$GRAYD;
 border: 4px colors.$GRAYD solid;
 border-radius: 60px;

 .item {
   @include flexbox.flexbox(center, center);
   z-index: 10;
   width: 100%;
   height: 60px;
   font-weight: 600;
   color: colors.$GRAYA;
   cursor: pointer;

   &.selected {
     color: colors.$BLACK;
   }
 }

 .box {
   position: absolute;
   width: 50%;
   height: 100%;
   margin: auto;
   background-color: white;
   border-radius: 60px;
   transition: all 0.3s linear;
   transform: translateX(0%);

   &.xEnd {
     transform: translateX(100%);
   }
 }
}

refactoring 전에는 tsx 파일안에 css를 쓰는 tailwindcss 라이브러리를 사용하여 css를 정리하였는데 refactoring과정에서는 jsx파일과 css파일을 분리하여 정리하였습니다.

Tab

before refactoring

import React, { useState } from "react";
import { cls } from "../libs/utils";

interface IItem {
 text: string;
 select: boolean;
 setItem: () => void;
}

interface IState {
 text: string;
 location: number;
}

const Item: React.FC<IItem> = ({ text, select, setItem }) => {
 return (
   <div
     className={cls(
       "cursor-pointer font-extrabold text-lg transition ease-in-out delay-100 w-52 h-12 flex justify-center items-center",
       select ? "" : "text-gray-400"
     )}
     onClick={setItem}
   >
     {text}
   </div>
 );
};

const translateXLocation = (location: number): string => {
 if (location === 0) {
   return "-translate-x-1";
 } else if (location === 1) {
   return "translate-x-52";
 }
 return "translate-x-[400px]";
};

const Tab = () => {
 const items = ["감자", "고구마", "카레라이스"];
 const [select, setSelect] = useState<IState>({ text: items[0], location: 0 });

 const setItem = (item: string, idx: number) =>
   setSelect({ text: item, location: idx });

 return (
   <div>
     <div className="flex relative">
       {items.map((item, idx) => (
         <Item
           key={idx}
           text={item}
           select={select.text === item}
           setItem={() => setItem(item, idx)}
         />
       ))}
       <div className="absolute w-full bg-gray-300 h-1 -bottom-3 px-4 ">
         <div
           className={cls(
             "bg-teal-500 w-1/3 h-full transition ease-in-out delay-150",
             translateXLocation(select.location)
           )}
         />
       </div>
     </div>
   </div>
 );
};

export default Tab;

after

import { useState } from 'react'
import styles from './Tab.module.scss'

const data = ['감자', '고구마', '카레라이스']

const Tab = () => {
 const [selectedIdx, setSelectedIdx] = useState<number>(0)
 const handleClick = (idx: number) => {
   setSelectedIdx(idx)
 }
 return (
   <div className={styles.container}>
     <div className={styles.itemContainer}>
       {data.map((item, idx) => (
         <button key={item} className={styles.item} type='button' onClick={() => handleClick(idx)}>
           {item}
         </button>
       ))}
     </div>
     <div className={styles.indicatorContainer}>
       <div
         className={styles.indicator}
         style={{
           transition: 'all 0.2s linear',
           transform: `translateX(${selectedIdx * 100}%)`,
         }}
       />
     </div>
   </div>
 )
}

export default Tab
@use '/src/styles/constants/colors';
@use '/src/styles/mixins/flexbox';

.container {
 width: 450px;

 .itemContainer {
   @include flexbox.flexbox(between, center);

   .item {
     @include flexbox.flexbox(center, center);
     width: 100%;
     padding: 10px 0;
     font-size: 20px;
     font-weight: 600;
     cursor: pointer;
   }
 }

 .indicatorContainer {
   width: 100%;
   height: 6px;
   background-color: colors.$GRAYD;

   .indicator {
     width: 33.3333%;
     height: 100%;
     background-color: colors.$TEAL;
   }
 }
}

eslint 규칙에 맞게 변경하였으며, 전에는 data가 추가 되거나 삭제되었을 경우를 생각하지 않고 코딩하였지만 피드백 후 확장성을 고려하여 css animation 효과를 줬습니다. 전에는 px 값를 주어 translateX를 사용했지만 후에는 %값을 주어 확장성까지 고려하였습니다.

Slider

before

import React, { useState } from "react";
import { cls } from "../libs/utils";

interface IInputValue {
  value: string;
  onClickValue: () => void;
}

const InputValue: React.FC<IInputValue> = ({ value, onClickValue }) => {
  return (
    <div
      onClick={onClickValue}
      className="w-12 rounded-3xl cursor-pointer py-1 flex justify-center items-center bg-gray-200 text-xs text-gray-500 hover:text-white hover:bg-teal-600"
    >
      {value}
    </div>
  );
};

const RoundValue: React.FC<{ value: number; standard: number }> = ({
  value,
  standard,
}) => {
  return (
    <div
      className={cls(
        value > standard ? "bg-teal-600" : "bg-gray-200",
        "w-4 h-4 rounded-full"
      )}
    />
  );
};

const Slider = () => {
  const [value, setValue] = useState<string>("0");

  return (
    <div>
      <div className="w-96 py-3 px-2 flex justify-end items-center bg-gray-100 border-[1.5px] border-gray-200 rounded-md mb-5">
        <div className="text-lg font-semibold">{value}</div>
        <div className="text-gray-400 text-sm ml-7">%</div>
      </div>
      <div className="relative">
        <input
          id="myRange"
          type="range"
          min="0"
          max="100"
          list="number"
          step={1}
          className={cls(
            "w-96 accent-teal-600 bg-transparent focus:shadow-none appearance-none h-1 rounded-full z-50",
            value === "0" ? "accent-gray-200" : ""
          )}
          value={value}
          onChange={(event) => setValue(event.target.value)}
        />
        <div className="absolute w-96 bg-transparent h-2 z-10 top-1/2 rounded-full pointer-events-none">
          <div
            className="bg-teal-600 h-2 rounded-full overflow-hidden"
            style={{ width: `${value}%` }}
          ></div>
        </div>

        <div className="bg-gray-200 h-2 absolute top-1/2 rounded-full -z-10 w-full flex justify-between items-center">
          <RoundValue value={+value} standard={0} />
          <RoundValue value={+value} standard={25} />
          <RoundValue value={+value} standard={50} />
          <RoundValue value={+value} standard={75} />
          <RoundValue value={+value} standard={100} />
        </div>
      </div>
      <div className="mt-5 flex justify-between items-center w-96">
        <InputValue value="0%" onClickValue={() => setValue("0")} />
        <InputValue value="25%" onClickValue={() => setValue("25")} />
        <InputValue value="50%" onClickValue={() => setValue("50")} />
        <InputValue value="75%" onClickValue={() => setValue("75")} />
        <InputValue value="100%" onClickValue={() => setValue("100")} />
      </div>
    </div>
  );
};

export default Slider;

after

import { useState } from 'react'
import { cx } from '../../styles'
import styles from './Slider.module.scss'

interface IInputValue {
  value: string
  onClickValue: () => void
}

interface IRoundValue {
  value: number
  standard: number
}

const InputValue = ({ value, onClickValue }: IInputValue) => {
  return (
    <button onClick={onClickValue} className={styles.inputValue} type='button'>
      {value}
    </button>
  )
}

const RoundValue = ({ value, standard }: IRoundValue) => {
  return <div className={cx({ [styles.selectedRoundValue]: value >= standard && value !== 0 }, styles.roundValue)} />
}

const Slider = () => {
  const [rangeValue, setRangeValue] = useState<number>(0)

  const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setRangeValue(+e.currentTarget.value)
  }

  const handleClickValue = (value: number) => {
    setRangeValue(value)
  }

  return (
    <div className={styles.container}>
      <div className={styles.valueContainer}>
        <span className={styles.valueContainerValue}>{rangeValue}</span>
        <span className={styles.percentage}>%</span>
      </div>
      <div className={styles.rangeContainer}>
        <input
          type='range'
          min='0'
          max='100'
          list='number'
          step={1}
          className={cx({ [styles.myRangeGRAYA]: rangeValue === 0 }, styles.myRange)}
          value={rangeValue}
          onChange={handleChangeInput}
        />
        <div className={styles.rangeInputContainer}>
          <div
            className={cx(styles.rangeInputWidth, { [styles.myRangeGRAYA]: rangeValue === 0 })}
            style={{ width: `${rangeValue}%` }}
          />
        </div>
        <div className={styles.ballRangeContainer}>
          <RoundValue value={rangeValue} standard={0} />
          <RoundValue value={rangeValue} standard={25} />
          <RoundValue value={rangeValue} standard={50} />
          <RoundValue value={rangeValue} standard={75} />
          <RoundValue value={rangeValue} standard={99} />
        </div>
      </div>
      <div className={styles.inputValueContainer}>
        <InputValue value='0' onClickValue={() => handleClickValue(0)} />
        <InputValue value='25' onClickValue={() => handleClickValue(25)} />
        <InputValue value='50' onClickValue={() => handleClickValue(50)} />
        <InputValue value='75' onClickValue={() => handleClickValue(75)} />
        <InputValue value='100' onClickValue={() => handleClickValue(100)} />
      </div>
    </div>
  )
}

export default Slider
@use '/src/styles/constants/colors';
@use '/src/styles/mixins/flexbox';

.container {
  flex-direction: column;
  row-gap: 25px;
  width: 500px;
  @include flexbox.flexbox(center, center);

  .valueContainer {
    @include flexbox.flexbox(end, center);
    width: 80%;
    height: 50px;
    padding: 0 20px;
    background-color: colors.$GRAYE;
    border-radius: 5px;

    .valueContainerValue {
      margin-right: 8px;
      font-weight: 700;
    }

    .percentage {
      color: colors.$GRAYA;
    }
  }

  .rangeContainer {
    position: relative;
    width: 100%;

    .myRange {
      z-index: 10000000;
      width: 100%;
      height: 4px;
      border-radius: 50%;

      &:focus {
        box-shadow: none;
      }
    }

    .myRange::-webkit-slider-thumb {
      -webkit-appearance: none;
      appearance: none;
      width: 25px;
      height: 25px;
    }

    .myRange::-moz-range-thumb {
      z-index: 150;
      width: 30px;
      height: 30px;
      cursor: pointer;
      background: colors.$TEAL;
      border-radius: 50%;

      &.myRangeGRAYA {
        background: colors.$GRAYA;
      }
    }

    .rangeInputContainer {
      position: absolute;
      top: 50%;
      z-index: 10;
      width: 100%;
      height: 10px;
      pointer-events: none;

      .rangeInputWidth {
        height: 10px;
        pointer-events: none;
        background-color: colors.$TEAL;
        border-radius: 10px;
      }
    }

    .ballRangeContainer {
      @include flexbox.flexbox(between, center);
      position: absolute;
      top: 50%;
      z-index: 0;
      width: 100%;
      height: 10px;
      pointer-events: none;
      background-color: colors.$GRAYA;
      border-radius: 10px;

      .roundValue {
        z-index: -10;
        width: 23px;
        height: 23px;
        background-color: colors.$GRAYA;
        border-radius: 50%;

        &.selectedRoundValue {
          background-color: colors.$TEAL;
        }
      }
    }
  }

  .inputValueContainer {
    width: 105%;
    @include flexbox.flexbox(between, center);

    .inputValue {
      width: 48px;
      padding: 4px 0;
      font-weight: 700;
      color: colors.$GRAY6;
      cursor: pointer;
      background-color: colors.$GRAYD;
      border-radius: 8px;
      @include flexbox.flexbox(center, center);

      &:hover {
        color: colors.$WHITE;
        background-color: colors.$TEAL;
      }
    }
  }
}

event handler 함수를 따로 빼고 작성하였다. 전에는 짧은거는 굳이 뺄 필요 없다고 생각을 했지만 나중에 usecallback과 같은 함수를 사용한다거나 하는 refactoring 과정을 위해 빼놓는게 무조건 좋을 것 같다.

Input

import React, { useEffect, useState } from "react";
import { cls } from "../libs/utils";

const EmailCheckerSvg: React.FC<{ emailCorrect: boolean }> = ({
  emailCorrect,
}) => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      className={cls(
        "h-5 w-5 absolute top-3 right-2",
        emailCorrect ? "text-teal-500" : "text-gray-500"
      )}
      viewBox="0 0 20 20"
      fill="currentColor"
    >
      <path
        fillRule="evenodd"
        d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
        clipRule="evenodd"
      />
    </svg>
  );
};

const EyeSvg: React.FC<{ className: string; toggleShow: () => void }> = ({
  className,
  toggleShow,
}) => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      className={className}
      viewBox="0 0 20 20"
      fill="currentColor"
      onClick={toggleShow}
    >
      <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
      <path
        fillRule="evenodd"
        d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
        clipRule="evenodd"
      />
    </svg>
  );
};

const EyeOffSvg: React.FC<{ className: string; toggleShow: () => void }> = ({
  className,
  toggleShow,
}) => {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      className={className}
      viewBox="0 0 20 20"
      fill="currentColor"
      onClick={toggleShow}
    >
      <path
        fillRule="evenodd"
        d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z"
        clipRule="evenodd"
      />
      <path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
    </svg>
  );
};

const InputError: React.FC<{ error: string }> = ({ error }) => {
  return <span className="text-xs text-red-500">{error}</span>;
};

const Input = () => {
  const [email, setEmail] = useState<string | null>();
  const [emailError, setEmailError] = useState<string | null>();
  const [emailCorrect, setEmaiCorrect] = useState<boolean>(false);

  const [showPassword, setShowPassword] = useState<boolean>(false);

  const toggleShowPassword = () => setShowPassword(true);
  const toggleUnShowPassword = () => setShowPassword(false);

  // eslint-disable-next-line react-hooks/exhaustive-deps
  var regEmail =
    // eslint-disable-next-line no-useless-escape
    /^[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_\.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/;

  useEffect(() => {
    if (email) {
      setEmaiCorrect(regEmail.test(email));
    }
  }, [email, regEmail]);

  return (
    <div className="space-y-2 w-96">
      <div className="flex flex-col space-y-2">
        <label className="text-xs text-gray-500">Email</label>
        <div className="relative w-full">
          <input
            type="text"
            className="p-2 bg-gray-50 focus:bg-gray-100 focus:outline-none rounded-sm shadow-sm w-full"
            placeholder="E-Mail"
            onChange={(event) => {
              setEmail(event.target.value);
            }}
            onBlur={() => {
              if (email) {
                if (regEmail.test(email)) {
                  setEmailError(null);
                } else {
                  setEmailError("Invaild e-mail address");
                }
              }
            }}
          />
          <EmailCheckerSvg emailCorrect={emailCorrect} />
        </div>
        {emailError && <InputError error={emailError} />}
      </div>
      <div className="flex flex-col space-y-2">
        <label className="text-xs text-gray-500">Password</label>
        <div className="relative w-full">
          <input
            type={showPassword ? "text" : "password"}
            className="p-2 bg-gray-50 focus:bg-gray-100 focus:outline-none rounded-sm shadow-sm w-full"
            placeholder="Password"
          />
          {showPassword ? (
            <EyeSvg
              className="h-5 w-5 absolute top-3 right-2 text-teal-500"
              toggleShow={toggleUnShowPassword}
            />
          ) : (
            <EyeOffSvg
              className="h-5 w-5 absolute top-3 right-2 text-gray-500"
              toggleShow={toggleShowPassword}
            />
          )}
        </div>
      </div>
    </div>
  );
};

export default Input;

after

import { useEffect, useState } from 'react'
import styles from './Input.module.scss'
import { CheckIcon, EyeIcon } from '../../assets/svgs'
import { cx } from '../../styles'

const EMAIL_REG_TEST = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/

const Input = () => {
  const [emailValue, setEmailValue] = useState<string | null>()
  const [emailError, setEmailError] = useState<string | null>()
  const [emailCorrect, setEmailCorrect] = useState<boolean>(false)

  const [showPassword, setShowPassword] = useState<boolean>(false)

  const handleChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEmailValue(e.currentTarget.value)
  }

  const handleBlurEmail = () => {
    if (emailValue) {
      if (EMAIL_REG_TEST.test(emailValue)) {
        setEmailError(null)
      } else {
        setEmailError('Invalid e-mail address')
      }
    }
  }

  const handlePasswordButton = () => setShowPassword((prev) => !prev)

  useEffect(() => {
    if (emailValue) {
      setEmailCorrect(EMAIL_REG_TEST.test(emailValue))
    }
  }, [emailValue])

  return (
    <div className={styles.wrapper}>
      <div className={styles.inputContainer}>
        <label htmlFor='email'>Email</label>
        <div>
          <input
            id='email'
            type='email'
            placeholder='E-Mail'
            autoComplete='off'
            autoCapitalize='off'
            autoCorrect='off'
            onChange={handleChangeEmail}
            onBlur={handleBlurEmail}
          />
          <div className={styles.iconContainer}>
            <CheckIcon className={cx({ [styles.colorCorrect]: emailCorrect })} />
          </div>
        </div>
        {emailError && <span>{emailError}</span>}
      </div>
      <div className={styles.inputContainer}>
        <label htmlFor='password'>Password</label>
        <div>
          <input
            id='password'
            type={showPassword ? 'text' : 'password'}
            placeholder='Password'
            autoComplete='off'
            autoCapitalize='off'
            autoCorrect='off'
          />
          <div className={styles.iconContainer}>
            <button type='button' onClick={handlePasswordButton}>
              <EyeIcon className={cx({ [styles.colorCorrect]: showPassword })} />
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Input
@use '/src/styles/constants/colors';

.wrapper {
  display: flex;
  flex-direction: column;
  row-gap: 15px;
  width: 450px;

  .inputContainer {
    display: flex;
    flex-direction: column;
    row-gap: 8px;

    label {
      font-size: 15px;
      color: colors.$GRAY3;
    }

    div {
      position: relative;
      width: 100%;

      input {
        width: 100%;
        padding: 8px;
        background-color: colors.$GRAYE;
        border-radius: 5px;
        box-shadow: rgba(0, 0, 0, 5%) 0 6px 24px 0, rgba(0, 0, 0, 8%) 0 0 0 1px;
      }

      .iconContainer {
        svg {
          position: absolute;
          top: -28px;
          right: 2%;
          color: colors.$GRAYA;
          cursor: pointer;

          &.colorCorrect {
            color: colors.$TEAL;
          }
        }
      }
    }

    span {
      font-size: 15px;
      font-weight: 600;
      color: colors.$RED;
    }
  }
}

var사용을 const 사용으로 바꾸고 email regex 변수를 w3c 사이트에 정해준 기준을 사용해야 한다. 옛날 이상한 이메일도 허용시켜주기 위해서이다.

before

import React, { useState } from "react";

const Dropdown = () => {
  const data = [
    "All Symbols",
    "BTCUSD.PERP",
    "ETHUSD.PERP",
    "BCHUSER.PERP",
    "LTCUSD.PERP",
    "XRPUSD.PERP",
    "1000SHIBUSD.PERP",
    "BANDUSD.PERP",
  ];
  const [value, setValue] = useState<string>(data[0]);
  const [showSearch, setShowSearch] = useState<boolean>(false);
  const [searchValue, setSearchValue] = useState<string>("");
  return (
    <div className="w-96 space-y-2">
      <div
        onClick={() => setShowSearch((prev) => !prev)}
        className="w-full h-16 bg-gray-100 text-lg font-semibold mb-5 flex justify-between items-center px-4 text-gray-500 cursor-pointer border-[2px] rounded-md"
      >
        <div>{value}</div>
        <div></div>
      </div>
      {showSearch && (
        <div className="relative">
          <svg
            xmlns="http://www.w3.org/2000/svg"
            className="h-6 w-6 absolute top-5 left-2 text-gray-400"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
            strokeWidth={2}
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
            />
          </svg>
          <input
            className="bg-gray-50 border-[2px] w-full h-16 rounded-t-md outline-none pr-2 pl-10 text-xl text-gray-500"
            value={searchValue}
            placeholder="Search Symbol"
            onChange={(event) => setSearchValue(event.target.value)}
          />
          <ul className="bg-gray-50 border-[2px] border-t-0 space-y-2 rounded-b-md">
            {data
              .filter(
                (symbolName) =>
                  symbolName.toLocaleLowerCase().startsWith(searchValue) ||
                  symbolName.toLocaleUpperCase().startsWith(searchValue)
              )
              .map((symbolName, idx) => (
                <li
                  key={idx}
                  className="pl-10 text-xl py-4 hover:bg-gray-100 cursor-pointer text-gray-500 font-medium"
                  onClick={() => setValue(symbolName)}
                >
                  {symbolName}
                </li>
              ))}
          </ul>
        </div>
      )}
    </div>
  );
};

export default Dropdown;

after

import { useRef, useState } from 'react'
import useOnClickOutside from '../../hooks/useOnClickOutside'
import styles from './Dropdown.module.scss'

const data = [
  'All Symbols',
  'BTCUSD.PERP',
  'ETHUSD.PERP',
  'BCHUSER.PERP',
  'LTCUSD.PERP',
  'XRPUSD.PERP',
  '1000SHIBUSD.PERP',
  'BANDUSD.PERP',
]

const Dropdown = () => {
  const [value, setValue] = useState<string>(data[0])
  const [showSearch, setShowSearch] = useState<boolean>(false)
  const [searchValue, setSearchValue] = useState<string>('')

  const handleClickShowSearch = () => setShowSearch((prev) => !prev)

  const handleClickSymbolName = (symbolName: string) => setValue(symbolName)

  const ref = useRef<HTMLDivElement>(null)

  useOnClickOutside<HTMLDivElement>({ ref, handler: () => setShowSearch(false) })

  return (
    <div className={styles.wrapper}>
      <button onClick={handleClickShowSearch} className={styles.container} type='button'>
        <div>{value}</div>
        <div></div>
      </button>
      {showSearch && (
        <div className={styles.searchContainer} ref={ref}>
          <svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
            <path strokeLinecap='round' strokeLinejoin='round' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z' />
          </svg>
          <input
            value={searchValue}
            placeholder='Search Symbol'
            onChange={(event) => setSearchValue(event.target.value)}
          />
          <ul>
            {data
              .filter(
                (symbolName) =>
                  symbolName.toLocaleLowerCase().includes(searchValue) ||
                  symbolName.toLocaleUpperCase().includes(searchValue)
              )
              .map((symbolName) => (
                <button key={symbolName} onClick={() => handleClickSymbolName(symbolName)} type='button'>
                  {symbolName}
                </button>
              ))}
          </ul>
        </div>
      )}
    </div>
  )
}

export default Dropdown
@use '/src/styles/constants/colors';
@use '/src/styles/mixins/flexbox';

.wrapper {
  row-gap: 8px;
  width: 450px;

  .container {
    @include flexbox.flexbox(between, center);
    width: 100%;
    height: 64px;
    padding: 0 16px;
    margin-bottom: 20px;
    font-size: large;
    font-weight: 600;
    color: colors.$GRAY6;
    cursor: pointer;
    background-color: colors.$GRAYE;
    border: 2px solid;
    border-radius: 5px;
  }

  .searchContainer {
    position: relative;

    svg {
      position: absolute;
      top: 13px;
      left: 8px;
      width: 24px;
      height: 24px;
      color: colors.$GRAY6;
    }

    input {
      width: 100%;
      height: 50px;
      padding-right: 8px;
      padding-left: 40px;
      font-size: large;
      color: colors.$GRAY6;
      background-color: colors.$GRAYE;
      border: 2px solid;
      border-top-left-radius: 5px;
      border-top-right-radius: 5px;
    }

    ul {
      @include flexbox.flexbox(start, start);
      flex-direction: column;
      row-gap: 10px;
      padding: 4px 10px;
      color: colors.$GRAY6;
      background-color: colors.$GRAYE;
      border: 2px solid;
      border-top: 0;
      border-bottom-right-radius: 5px;
      border-bottom-left-radius: 5px;

      button {
        font-size: large;
        font-weight: 500;
        color: colors.$GRAY6;
        cursor: pointer;
      }
    }
  }
}

startsWith를 검색기능을 만든 것을 includes 함수로 변경하였다.
그리고 밖을 클릭 했을 시 dropdown이 꺼지는 기능을 만들어야 해서 use on click outside hook을 만들어서 기능을 추가하였다.

post-custom-banner

0개의 댓글