3주차(목) - 프리 온보딩 코스 프론트엔드 - 기업 과제 회고

minbr0ther·2022년 2월 19일
0

pre-onboarding-fe

목록 보기
13/15
post-thumbnail

이번 과제는 '영양제 검색 서비스'를 만드는 것이였다.

따로 제시된 디자인 은 없었고 UI/UX를 잘 생각해서 구현을 해야했다.

그래서 프로젝트 시작전 팀원들과 '검색 서비스'관련 정보를 리서치 해서 회의를 하는 시간을 가졌다.

  • 영양제는 브랜드명, 제품명으로 구성되어 있고 한글과 영어가 혼재되어 있습니다(브랜드명이 없는 경우도 다수 존재합니다).

  • 소비자가 찾고자 하는 키워드를 입력했을때 제품을 어떤 우선순위로 노출할지

👨🏻‍💻 프로젝트 세팅

  • CNA + TS template

  • ESLint, Prettier (+ TypeScript)

  • styled-components

  • TS 절대경로 세팅

  • components 나누기

  • git repo 생성

    • slack git subscribe pre-onboarding-course-team-6/{레포이름}

⚙️ 구현한 기능 설명

1. 검색어 자동완성 구현 및 Debounce

검색어를 입력하면 키워드 단위로 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 작동

2. 많은 키워드를 가지고 있는 검색어 추천

우리팀의 임시 백엔드 개발자?로 계신👀 선명님이 전체 물품을 키워드 단위로 쪼개서 키워드 중복이 많은 순에서 적은 순으로 보여주는 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;

3. 인피니티 스크롤

트렌드를 따라서 검색결과 보여주는 것을 페이지네이션을 인피니티 스크롤로 구현하였다.

라이브러리 사용이 자유라 '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> 등과 같은 이벤트 타입은 슬슬 불편하지 않게 느껴지고 있다. (그런데 아직 타입스크립트의 장점을 못느끼고 있다 그냥 불편하다.. 에러 좌라ㅏ락..🤣)

profile
느리지만 꾸준하게 💪🏻

0개의 댓글