원티드 프론트엔드 선발 과제 리팩토링 하기
Github: https://github.com/GyuJae/wanted_pre_onboarding_refatoring
URL: https://gyujae.github.io/wanted_pre_onboarding_refatoring/
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파일을 분리하여 정리하였습니다.
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를 사용했지만 후에는 %값을 주어 확장성까지 고려하였습니다.
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 과정을 위해 빼놓는게 무조건 좋을 것 같다.
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을 만들어서 기능을 추가하였다.