- ํ ์๊ฐ ๐ซ
- ํ๋ก์ ํธ ์๊ฐ ๐
- ๊ธฐ์ ์คํ ๐
- ๊ตฌํ ๊ธฐ๋ฅ ๐
- ํ๋ก์ ํธ ๊ตฌ์กฐ ๐
- Best Practice ์ ์ ๊ณผ์ ๐ฉโ๐ฆโ๐ฆ
- ํ๋ก์ ํธ ์ค์น ๋ฐ ์คํ โจ
- ๊ฐ์ : ์ํฐ๋ ํ๋ก ํธ์๋ ํ๋ฆฌ์จ๋ณด๋ฉ 7๊ธฐ 2ํ ๊ณผ์ 3-1 ์ค Best Practice
- ์ฃผ์ : ๊ฒ์์ฐฝ & ๊ฒ์์ด ์ถ์ฒ ๊ธฐ๋ฅ ๊ตฌํ
- ๊ธฐ๊ฐ : 2022.11.8 ~ 2022.11.11
- Typescript
- React
- Axios
- Styled-Components
- ๊ตฌํ์ฌํญ
- ์งํ๋ช
๊ฒ์์ API ํธ์ถ ํตํด์ ๊ฒ์์ด ์ถ์ฒ ๊ธฐ๋ฅ ๊ตฌํ
- ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ํ ์คํธ์ ์ผ์นํ๋ ๋ถ๋ถ ๋ณผ๋์ฒ๋ฆฌ
- ๊ฒ์์ด๊ฐ ์์ ์ โ๊ฒ์์ด ์์โ ํ์ถ
- API ํธ์ถ ์ต์ ํ
- API ํธ์ถ๋ณ๋ก ๋ก์ปฌ ์บ์ฑ ๊ตฌํ (๋ฏธ๊ตฌํ)
- ์ ๋ ฅ๋ง๋ค API ํธ์ถํ์ง ์๋๋ก API ํธ์ถ ํ์๋ฅผ ์ค์ด๋ ์ ๋ต ์๋ฆฝ ๋ฐ ์คํ
- ํค๋ณด๋๋ง์ผ๋ก ์ถ์ฒ ๊ฒ์์ด๋ค๋ก ์ด๋ ๊ฐ๋ฅํ๋๋ก ๊ตฌํ
- ์งํ๋ช
๊ฒ์์ API ํธ์ถ ํตํด์ ๊ฒ์์ด ์ถ์ฒ ๊ธฐ๋ฅ ๊ตฌํ
src
โฃ api // ๋น๋๊ธฐ ํต์ ๊ด๋ จ ๋ก์ง ๊ด๋ฆฌ
โฃ components // ๊ณต์ฉ ์ปดํฌ๋ํธ
โฃ constant // ์์ ๋ณ์ ๊ด๋ฆฌ
โฃ data // ๋๋ฏธ ๋ฐ์ดํฐ ๊ด๋ฆฌ
โฃ hooks // ์ปค์คํ
ํ
๊ด๋ฆฌ
โฃ pages // ํ์ด์ง ์ปดํฌ๋ํธ
โฃ style // ๊ธ๋ก๋ฒ ์คํ์ผ ๊ด๋ฆฌ
โฃ types // ๊ณต์ฉ ํ์
๊ด๋ฆฌ
โ utils // ๊ณต์ฉ ์ ํธ ํจ์ ๊ด๋ฆฌ
// src/hooks/useDebounce.ts
import { useEffect, useState } from "react";
export const useDebounce = (value: string, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
};
// src/components/SearchForm.ts
export const SearchForm = () => {
const [value, onChange, reset] = useInput("");
const debouncedValue = useDebounce(value, 500);
const [{ loading, data, error }] = useSickAsync(debouncedValue, [
debouncedValue,
]);
// ...
- ์ ๋ ฅ๋ง๋ค API ํธ์ถํ์ง ์๋๋ก API ํธ์ถ ํ์๋ฅผ ์ค์ด๊ธฐ ์ํด Debounce ํจ์ ์ ์ฉ
- ์ปค์คํ ํ ์ผ๋ก ๋ถ๋ฆฌ (useDebounce)
- ์ฌ์ฉ์ ์ ๋ ฅ๊ฐ์ Debounce๋ ๊ฐ์ผ๋ก api ์์ฒญ(useSickAsync)
// src/hooks/useSickAsync.ts
import { AxiosError, AxiosResponse } from "axios";
import { DependencyList, useEffect, useReducer } from "react";
import { httpClient } from "../api/api";
import { SickServiceImp } from "../api/SickService";
export type AsyncData = AxiosResponse<any, any> | null;
export type AsyncError = AxiosError | Error | null | boolean;
export type RequestState = {
loading: boolean;
data: AsyncData;
error: AsyncError;
};
export type Action =
| { type: "LOADING" }
| { type: "SUCCESS"; data: AsyncData }
| { type: "ERROR"; error: AsyncError };
type Cache = Record<string, AsyncData>;
const cache: Cache = {};
function asyncReducer(state: RequestState, action: Action): RequestState {
switch (action.type) {
case "LOADING":
return {
loading: true,
data: null,
error: null,
};
case "SUCCESS":
return {
loading: false,
data: action.data,
error: null,
};
case "ERROR":
return {
loading: false,
data: null,
error: action.error,
};
default:
throw new Error(`Unhandled action type`);
}
}
const initialState: RequestState = {
loading: false,
data: null,
error: false,
};
const sickService = new SickServiceImp(httpClient);
export const useSickAsync = (
value: string,
deps: DependencyList
): [RequestState, () => Promise<void>] => {
const [state, dispatch] = useReducer(asyncReducer, initialState);
const fetchData = async () => {
dispatch({ type: "LOADING" });
try {
const data = await sickService.getSickList({ q: value });
dispatch({ type: "SUCCESS", data });
} catch (e: any) {
dispatch({ type: "ERROR", error: e });
}
};
useEffect(() => {
fetchData();
}, deps);
return [state, fetchData];
};
// src/components/SearchForm.ts
export const SearchForm = () => {
const [value, onChange, reset] = useInput("");
const debouncedValue = useDebounce(value, 500);
const [{ loading, data, error }] = useSickAsync(debouncedValue, [
debouncedValue,
]);
const items = data?.data;
const [activeIdx, handleKeyArrow] = useKeyArrow(items || []);
return (
<S.SearchFormContainer>
<S.SearchFormTitle>
๊ตญ๋ด ๋ชจ๋ ์์์ํ ๊ฒ์ํ๊ณ ์จ๋ผ์ธ์ผ๋ก ์ฐธ์ฌํ๊ธฐ
</S.SearchFormTitle>
<SearchInput
value={value}
onChange={onChange}
reset={reset}
onKeyDown={handleKeyArrow}
/>
{value && (
<S.SearchResultBlock>
{loading && <S.SearchItemLabel>๊ฒ์ ์ค ...</S.SearchItemLabel>}
{error && <S.SearchItemError>์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค</S.SearchItemError>}
{items && (
<SearchList
value={debouncedValue}
items={items}
activeIdx={activeIdx}
/>
)}
</S.SearchResultBlock>
)}
</S.SearchFormContainer>
);
};
//
- Flux ํจํด์ ์ ์ฉํด ๋น๋๊ธฐ ํต์ ์ํ ๊ด๋ฆฌ(loading,error,success)๋ฅผ ์ปค์คํ ํ ์ผ๋ก ๋ถ๋ฆฌ
// src/utils
export const divideByKeyword = (target: string, keyword: string) => {
const preIdx = target.indexOf(keyword);
const postIdx = preIdx + keyword.length;
const prefix = target.slice(0, preIdx);
const postfix = target.slice(postIdx);
return [prefix, postfix];
};
// src/components/SearchItem.tsx
interface SearchItemProps {
value: string;
keyword: string;
isActive?: boolean;
}
export const SearchItem = ({
value,
keyword,
isActive = false,
}: SearchItemProps) => {
const [prefix, postfix] = divideByKeyword(value, keyword);
return (
<S.SearchItemBlock isActive={isActive}>
<S.SearchItemIcon>
<HiOutlineSearch size="1.8rem" color="#a6afb7" />
</S.SearchItemIcon>
<S.SearchItemText>{prefix}</S.SearchItemText>
<S.SearchItemMatched>{keyword}</S.SearchItemMatched>
<S.SearchItemText>{postfix}</S.SearchItemText>
</S.SearchItemBlock>
);
};
- ๊ฒ์์ด(
keyword
)๋ฅผ ๊ธฐ์ค์ผ๋ก ์ ์ฒด ํ ์คํธ(target
) string์ ๋ถ๋ฆฌํ๋ ์ ํธ ํจ์ ๊ตฌํ
- Git Clone
$ git clone https://github.com/pre-onboading-2team/pre-onboarding-7th-3-1-2.git
- ํ๋ก์ ํธ ํจํค์ง ์ค์น
$ npm install
- ํ๋ก์ ํธ ์คํ
$ npm start