이번 과제는 '영양제 검색 서비스'를 만드는 것이였다.
따로 제시된 디자인 은 없었고 UI/UX를 잘 생각해서 구현을 해야했다.
그래서 프로젝트 시작전 팀원들과 '검색 서비스'관련 정보를 리서치 해서 회의를 하는 시간을 가졌다.
영양제는 브랜드명, 제품명으로 구성되어 있고 한글과 영어가 혼재되어 있습니다(브랜드명이 없는 경우도 다수 존재합니다).
소비자가 찾고자 하는 키워드를 입력했을때 제품을 어떤 우선순위로 노출할지
CNA + TS template
ESLint, Prettier (+ TypeScript)
styled-components
TS 절대경로 세팅
components 나누기
git repo 생성
git subscribe pre-onboarding-course-team-6/{레포이름}
검색어를 입력하면 키워드 단위로 JSON server에 fetch를 날리는 기능을 구현하였다.
자동완성의 기본적인 기능만 구현하면 서버에 키보드를 한 번 한 번 누를 때마다 fetch를 요청해서 부하를 주게 된다.
그래서 이를 해결하기 위해서 나온 해결책인 Debounce 기능을 통해 해결할 수 있었다.
// App.tsx
const [input, setInput] = useState(''); //input이 변경되었을때를 가정
const debouncedValue = useDebounce<string>(input); // input의 값 useDebounce에 전달
// useDebounce.ts
import { useEffect, useState } from 'react';
// 범용성있게 사용하기 위해서 Generics 활용
function useDebounce<T>(value: T, delay?: number): T { //delay는 optional
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
// [value, delay]의 변경사항이 있으면 delay || 500시간동안 지연을 준다
// debouncedValue value값을 저장한다
const timer = setTimeout(() => setDebouncedValue(value), delay || 500);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
// 저장된 debouncedValue value값 반환
return debouncedValue;
}
export default useDebounce;
useEffect(() => {
const fetchData = async () => {
try {
setError(null);
setItems([]);
setLoading(true);
// 2. async/await을 통해서 검색어 query parmeter와 함께 서버에 get요청
const response = await axios.get(
`${MOCK_URL}/nutrients?keyword=${input}`,
);
const { data } = response;
// 3. 자동 완성된 검색결과를 저장한다.
setItems(data.nutrients);
setLoading(false);
} catch (err: unknown) {
if (err instanceof Error) {
return {
message: `Things exploded (${err.message})`,
};
}
setLoading(false);
}
};
fetchData();
}, [debouncedValue]); // 1. debouncedValue 변경되면 trigger 작동
우리팀의 임시 백엔드 개발자?로 계신👀 선명님이 전체 물품을 키워드 단위로 쪼개서 키워드 중복이 많은 순에서 적은 순으로 보여주는 API를 개발하셨다.
키워드가 많다 == 검색량이 많을 것이다 라는 상황을 가정하고 '추천 검색어'로 제공하게 되었다.
const [recommend, setRecommend] = useState([]);
useEffect(() => {
const fetchTag = async () => {
try {
setError(null);
const res = await GetData(`${MOCK_URL}/tags`);
// 여러 태그들 중 상위 10개 항목을 추려서 가져온다
setRecommend(res.tags.slice(0, 10));
} catch (err: unknown) {
if (err instanceof Error) {
return {
message: `Things exploded (${err.message})`,
};
}
setLoading(false);
}
};
fetchTag();
}, []);
import React from 'react';
import * as S from './styled';
interface Tag {
tag: string;
count: number;
}
type Props = {
handleOnClick: () => void;
onSubmit: (
e: React.FormEvent<HTMLFormElement> | React.MouseEvent<HTMLButtonElement>,
value?: string,
) => void;
recommend: Tag[];
};
const SelectBox: React.FC<Props> = ({recommend, onSubmit}) => {
// 클릭시 검색이 되게 함
const handleOnClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
onSubmit(e, e.currentTarget.value);
};
return (
<ul>
// 추천검색어를 map으로 뿌려준다.
{recommend.map((tag, index) => (
<button
key={index}
value={tag.tag}
onClick={handleOnClick}
>{`${tag.tag}`}</button>
))}
</ul>
);
};
export default SelectBox;
트렌드를 따라서 검색결과 보여주는 것을 페이지네이션을 인피니티 스크롤로 구현하였다.
라이브러리 사용이 자유라 'react-infinite-scroll-component'를 선택 및 적용하게 되었다.
타입스크립트로 적용해야해서 걱정이 있었지만 다행이게도 어려운 이슈는 없었다.
// useEffect에서 data fetch 해올때 response.pagination.next로 저장
const [token, setToken] = useState(null);
const [hasMore, setHasMore] = useState(true);
const getNextPage = async () => {
// 토큰(next page) 쿼리를 적용한 url을 fetch 한다.
const response = await axios.get(`${MOCK_URL}${token}`);
const result = response.data;
const data: Items[] = result.nutrients;
setView([...view, ...data]); // 받아온 데이터를 추가해준다
setToken(result.pagination.next); // 다음 페이지를 준비한다
};
useEffect(() => {
// 받아온 token이 null이면 추가 로드(hasMore)를 방지한다
token === null ? setHasMore(false) : setHasMore(true);
}, [token]);
<InfiniteScroll
dataLength={view.length} // 데이터의 총 개수
next={getNextPage} // 다음 60개의 데이터를 불러온다
hasMore={hasMore} // 다음페이지가 있는지 없는지
loader={<Loading />} // 로딩시 보여줄 컴포넌트
endMessage={ // 마지막에 노출할 수 있는 컴포넌트
<p style={{ textAlign: 'center' }}>
<b>모든 상품을 불러왔습니다.</b>
</p>
}
>
{view.length ? (
<S.ItemList>
{view.map((item, index) => (
<S.ItemWrap key={index}>
<S.ItemsBrand>{item.브랜드}</S.ItemsBrand>
<S.ItemsName>{item.제품명}</S.ItemsName>
</S.ItemWrap>
))}
</S.ItemList>
) : (
<Loading />
)}
</InfiniteScroll>
컴포넌트 단위로 나눠서 개발하고 싶지만 나누기가 애매해서 이번에도 짝짝코딩을 진행했다 (4명이 동시에 코딩👀) 역시나 처음부터 사용했던 게더타운을 사용해서 진행했다. 개발을 하면서 소통에 문제가 있으면 임시로 '라이브 쉐어' 기능을 이용해서 드라이버를 직접적으로 도우면서 했다.
'줌 피로도'라는 단어가 있다. 개발시간이 길어지면서 이를 실제로 느낄 수 있었다. 어제만 해도 팀원들과 같은시간에 대면으로 소통했다면 효율이 최소 1.5배는 더 좋았을 것 같다. 장점과 단점이 혼재하는 이방식은 '공동학습'을 하는데 정말 좋은 것 같다. 해본적이 없다면 꼭 추천한다👏🏻
2주 전부터 타입스크립트를 적용하면서 조금씩 감을 익혀가는중이다. 아직은 컴포넌트 단위로 나눠서 props를 내려줄때 두렵다. type | interface 정의에 아직 익숙하지 않다.
하지만 cheatsheet와 stack overflow를 통해서 열심히 극복해내고 있다. 반복적으로 사용하면서 React.MouseEvent<HTMLButtonElement>
등과 같은 이벤트 타입은 슬슬 불편하지 않게 느껴지고 있다. (그런데 아직 타입스크립트의 장점을 못느끼고 있다 그냥 불편하다.. 에러 좌라ㅏ락..🤣)