Yalp 프로젝트

개발 log·2022년 1월 31일
0

프로젝트

목록 보기
5/6

Yelp Clone Project

Git Hub 주소


🗓 진행 기간

2022.01.07 ~ 2022.01.28



🔨 사용 기술



📄 프로젝트 소개

이번 프로젝트는 특별한 기능 구현보다는 공부에 목적을 둔 프로젝트이며 목표는 아래와 같습니다.



🎯 프로젝트 목표

이론으로만 공부한 리액트와 리덕스를 제대로 사용하는 법을 숙지하고 프론트엔드 개발자로서 신경써야하는 부분을 생각하며, 완성도에 집중하고자 하였습니다.

  1. 컴포넌트를 분리하는 기준에 대해 명확하게 알기

    • 도메인 방식
    • Container Component & Presentation Component
    • Atomic Design
  2. 렌더링 최적화

    • code splitting(lazy & suspence)
    • manage dependency array
    • use memo
  3. 웹 접근성

    • 키보드 접근성 향상(트래핑)
    • 스크린 리더기 지원
  4. SEO (Search Engine Optimization)

    • React Helmet을 사용하여 검색엔진 최적화 하기
  5. Redux로 상태관리와 비동기 통신 제어하기

    • RTK Query
    • createAsyncThunk
    • 미들웨어를 사용해야하는 이유 제대로 알기


🎮 수행한 역할

1. 레스토랑 리스트 구현

Yelp API에서 받는 데이터를 기반으로 레스토랑 리스트를 구현했습니다.

2. 레스토랑 리스트와 연동되는 지도 구현

레스토랑 리스트를 기반으로 그려지는 지도를 구현했습니다.

  • 레스토랑 카드에 hovermap marker highlighting
  • map markerhoverhighlighting + restaurant info 제공


🏆 프로젝트를 통해 배운점

1. React에서 Proxy사용하기

  • cra에서 안내하는 proxy 사용법

package.json

{
  ...
  "proxy": "http://localhost:4000",
  ...
}

cra에서는 package.json에 위와 같은 내용을 추가하면 fetch('/api/todos');와 같은 요청에 위에서 설정한 origin으로 대체해준다고 기술되어 있습니다.

프로젝트에서 proxy서버가 필요했던 이유

처음에는 client측에서 요청을 보냈을 때 응답을 잘 해주는지 확인하기 위해 알맞는 URI를 모두 입력하고 요청을 보내보았으나 CORS에러가 발생했습니다.

  • yelp에서는 client측의 요청에 CORS Header를 제공하지 않기 때문에, 즉 Access-Collect-Allow-Origin을 제공하지 않기 때문에 CORS에러가 발생합니다.

  • 때문에 우리 팀은 proxy서버를 통해 요청을 우회하고자 하였습니다.

proxy에 대한 오해

  1. 리액트에서 proxy 설정을 하면 리액트 앱이 내부적으로 proxy서버를 구동하여 별개의 서버가 존재하는 줄 알았습니다.

    • 때문에 비어있는 Port번호를 proxy로 두고 사용했으나 proxy에러가 발생했습니다.
  2. 별도의 서버가 돌아가는 것이 아니라면 origin 그 자체를 바꿔 요청을 보내는 줄 알았습니다.

    • 때문에 yelp APIorigin으로 proxy를 설정하였으나 역시 CORS 에러가 발생했습니다.

react proxy의 역할

cra에서 사용하는 proxy 옵션의 역할은 cra에서 기술하는대로 요청 시 origin을 대체하여 주는 것입니다.

때문에 위에서 언급한 오해 1번은 origin은 대체했으나 존재하지 않는 서버에 요청을 보냈기 때문에 에러가 발생했고, 오해 2번은 origin을 대체해서 제대로 요청을 보냈지만 본질적으로 yelp에서 CORS Header를 제공하지 않으니 초기의 문제와 다르지 않은 상황이므로 CORS에러가 발생했던 것입니다.

결론

  • CORS Header를 제공해주는 proxy 서버를 구축하고 proxy서버에서 요청을 받으면 client에게 데이터를 전송해주는 방식으로 수정했습니다.

  • craproxy 옵션은 서버를 구동하거나 CORS을 해결하기 위해 존재한다기 보다는 요청 시에 origin을 바꿔주는 역할만 할 뿐입니다.


2. Redux에서 비동기 통신 제어하기

  • redux에 대한 이해가 부족한 상태에서 비동기 통신을 하려했기 때문에 redux에서 비동기 통신을 하기 위해서는 반드시 middleware를 사용해야 하는 줄 착각했습니다.

redux 아키텍처

redux 아키텍처 자체는 어려운 개념이 아니었습니다, 단순히 store는 상태를 관리하고 dispatchaction객체를 전달해 reducer를 통해 action을 수행하는 형식의 간단한 구조입니다.

middleware?

redux의 비동기 통신에 대한 예제를 설명할 때 대부분 middleware를 사용하며 설명하기에 redux에서 비동기 통신을 하기 위해선 반드시 middleware를 사용해야 하는 것이라고 착각했습니다.

하지만 middleware는 비동기 작업을 동기처럼 작업하기 위해 존재하는 것 뿐입니다.

비동기 통신 제어하기

단순히 비동기 통신을 하기 위해선 Promise의 후속 처리 메서드 then 내부에서 dispatch를 사용하거나 async, await을 사용한다면 데이터를 받은 후 dispatch를 하는 로직만 작성하면 됩니다.

그렇게 되면 비동기 작업이 완료된 후 dispatch가 실행되기 때문에 비동기 작업이 완료되었을 때 store를 업데이트 할 수 있습니다.

결론

  • redux에서 비동기 통신을 하기 위해서는 middleware가 필요한 것이 아니다.
  • 만약 비동기 통신의 순서를 보장받고 싶다면 middleware를 사용해야 한다.

3. e2e Test의 편의성

  • 이번 프로젝트에서는 storybook으로 e2e test를 했습니다.
  • 비록 시간적 문제로 인해 최소단위 컴포넌트만 e2e test를 진행했으나 간단한 기능만으로도 e2e test의 편의성을 알 수 있었습니다.
  1. 팀원간의 컴포넌트 공유가 빠르다.
  2. 예상치 못한 문제를 발견할 수 있다.
  3. CSS 수정이 용이하다.


🛒 프로젝트를 통해 성장한 점

1. 컴포넌트 설계

이번 프로젝트는 단순히 리액트를 사용하는 것에 목적을 두는 것이 아닌 리액트를 잘 사용해보자라는 취지로 진행한 프로젝트 였기에 컴포넌트 설계부터 신경썼습니다.

Atomic design 채택

저희가 컴포넌트 분리 기준을 atomic 관점으로 잡은 이유는 커스텀 훅을 잘 활용하면 상태에 따라 분류하는 과정이 용이해지게 되므로 표현 위주의 컴포넌트를 구현하게 되는데 이 과정에서 쟁점은 어떻게하면 표현 컴포넌트의 재사용성을 극대화하고 유지보수, 즉 잘게 쪼개진 컴포넌트의 탐색이 용이하게 할 수 있을까 였습니다.

이런 관점에서 태그의 집합 단위로 잘게 쪼개어 레이아웃과 페이지까지 그룹화할 수 있는 atomic이 표현 컴포넌트를 분리하는 기준으로 적합하다고 생각했습니다.

├─components
│  ├─common
│  ├─atoms
│  ├─molecules
│  ├─organisms
│  ├─templates
│  └─utils
├─hooks
├─pages

components

하나의 태그부터 레이아웃까지의 단계를 담은 폴더이며, 재사용성이 적은 페이지보다 재사용성을 염두에 둔 컴포넌트들의 집합입니다.

  1. atoms
  • 하나의 태그
  • 더 이상 나눠지지 않는 가장 작은 단위의 컴포넌트
  1. molecules
  • atoms의 집합
  • molecules를 구성하는 atomatoms 폴더에 반드시 존재해야하는 것은 아님
  1. organisms
  • atomsmolecules의 집합
  • 해당 단위부터는 독립적으로 사용될 수 있는 단위
  • 레이아웃을 구성하는 집합체
  1. templates
  • atomsmolecules, organisms의 집합
  • 해당 단위부터는 레이아웃을 구성하는 CSS가 작용
  • 독립적으로 사용 가능하나 컴포넌트 자체의 자유도는 organisms보다 낮음
  • 페이지를 구성하는 레이아웃의 개념
  1. common
  • templates단위에 포함되지만 독립적으로 분리되는 레이아웃이라기 보다는 페이지에 공통적으로 사용되는 레이아웃
  1. utils
  • 컴포넌트 자체가 특정 표현을 담당하는 것이 아닌 보조적인 역할의 컴포넌트의 집합

hooks

atomic 관점의 컴포넌트 분리를 표현적 관점에서 가능하도록 만든 핵심 폴더입니다.

  • 커스텀훅을 구현하여 hooks 폴더에서 관리하며 atomic 관점에서 분리된 컴포넌트들은 hooks에 구성되어 있는 커스텀 훅에 의존합니다.

  • 이렇게 하면 커스텀 훅을 사용하는 컴포넌트가 해당 상태에 의존하는 컴포넌트이므로 상태에 따른 분류가 명확해집니다.


pages

재사용관점보다는 components가 모여 이뤄낸 하나의 집합체, 즉 사용자에게 하나의 구성품으로 제공되는 페이지들의 집합체입니다.

  • 해당 폴더를 components에서 관리하지 않고 분리한 이유는 아래와 같습니다.

    1. SPA의 경우 router로 경로를 관리하는데 pages 폴더를 분리하면 routing되는 페이지를 명확하게 알 수 있습니다.
    2. pagescomponents가 의미하는 부품이라는 느낌보다는 부품이 모여 완성된 하나의 구성품이므로 분리를 하는 것이 논리적으로 옳다고 생각했습니다.


2. 성능 최적화

로딩 최적화

  1. Code splitting
    Router를 기준으로 React.lazy를 사용하여 하나의 bundlesplitting하였습니다.

    const App = lazy(() => import('pages/App/App'));
    const SearchPage = lazy(() => import('pages/SearchPage/SearchPage'));
    const DetailPage = lazy(() => import('pages/DetailPage/DetailPage'));
    const PageNotFound = lazy(() => import('pages/PageNotFound/PageNotFound'));
    
    ReactDOM.render(
      <StrictMode>
        <GlobalStyle />
        <HelmetProvider>
          <StoreProvider>
            <BrowserRouter>
              <Routes>
                <Route path="/" element={<Suspense fallback={<Circles />}><App /></Suspense>}>
                  <Route index element={<Navigate to={defaultQuery} replace={true} />} />
          	      <Route path="search" element={<Suspense fallback={<Circles />}><SearchPage /></Suspense>} />
          	      <Route path="restaurant/:id" element={<Suspense fallback={<Circles />}><DetailPage /></Suspense>} />
          	      <Route path="page-not-found" element={<Suspense fallback={<Circles  />}><PageNotFound /></Suspense>}/>
                  <Route path="*" element={<Navigate to="page-not-found" replace={true} />} />
    		    </Route>
              </Routes>
              <InitSVG />
            </BrowserRouter>
          </StoreProvider>
        </HelmetProvider>
      </StrictMode>,
      document.getElementById('root')
    );
    
  2. data lazy Loading
    Restaurant Card에서 사용되는 데이터를 받아올 때 Observer API를 사용하여 화면 상에 보이지 않는 Card는 데이터를 천천히 받아오게 하였습니다.

    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (!entry.isIntersecting) return;
    
        axios(`http://localhost:4001/api/businesses/${id}`).then(
          ({ data: { restaurantDetail, restaurantReview } }) => {
            let { hours } = restaurantDetail;
            hours = hours || [{ is_open_now: null, open: [initialOpen] }];
            
            const { reviews } = restaurantReview;
            const { is_open_now: isOpenNow, open } = hours[0];
    
            if (!open.find(({ day }) => day === today)) open.push(initialOpen);
            const { start, end } = open?.find(({ day }) => day === today);
    
            setReview(reviews[0].text);
            setOperationState({ isOpenNow, start, end });
          }
        );
        observer.unobserve(ref.current);
      });
    }, observerOptions);
    
    observer.observe(ref.current);

메모리 최적화

  1. React.memo

    memoization된 컴포넌트를 반환하도록 도와주는 React.memo를 활용하여 props이 변경되지 않는다면 리렌더링하지 않도록 제어하였습니다.

  2. RTK Query

    비동기 처리된 결과를 캐싱해주는 SWR을 차용한 Redux-toolkitRTK Query를 사용하여 데이터 제어하였습니다.

렌더링 최적화

  1. useEffectdependency 배열 관리

    컴포넌트의 업데이트 기준이 되는 state 혹은 propsdependency배열에서 정확히 지정하여 불필요한 리렌더링을 방지하였습니다.



3. 검색 엔진 최적화

React Helmet 활용

컴포넌트 내부에서 head 태그에 접근할 수 있는 react-helmet-async를 사용하여 검색엔진의 탐색을 도와주는 meta태그를 작성하여 검색엔진에 최적화하도록 노력했습니다.

  • SearchPage
<Helmet>
  <title>The Best 10 Restaurants in {location}</title>
  <meta name="description" content={`The Best 10 Restaurants in ${location}`} />
  <meta name="robots" content="ALL" />
  <meta name="keywords" content={`yalp, restaurant, ${location}`} />
  <meta name="author" content="Dumboz" />
  <meta name="content-language" content="en" />
  <meta httpEquiv="content-type" content="text/html; charset=en" />
  <meta name="keywords" content={keywords} />
</Helmet>
  • DetailPage
<Helmet>
  <title>{restaurantDetail?.name || 'Detail Page'}</title>
  <meta name="robots" content="ALL" />
  <meta name="keywords" content={`yalp, restaurant, ${restaurantDetail?.name}`} />
  <meta name="author" content="Dumboz" />
  <meta name="content-language" content="en" />
  <meta httpEquiv="content-type" content="text/html; charset=en" />
  <meta name="description" content={restaurantDetail?.name || 'Detail Page'} />
</Helmet>


4. Git Hub & Git

Git Hub Wiki

  • 매일 Daily Scrum을 Wiki에 기록하며 팀원이 한일과 자신이 한일을 명확하게 할 수 있도록 하며, 목표를 확실히 하여 프로젝트의 진행을 원활하게 하였습니다.

  • 프로젝트를 진행하며 기술적으로 어려웠던 부분은 기술로그에 정리하며 어려웠던 부분을 정리하며 지식을 확실히하고 복기할 수 있도록 하였습니다.

Git Commit

  • Commit Message를 작성할 때 Issue태그와 함께 작성하고, task단위로 commit을 작성하여 어떤 Issue에서 무슨 일을 했는지 자세히 알 수 있도록 하였습니다.

Code Review

  • 팀원들과 PR을 통해 Code Review를 진행하며 의견을 공유했습니다.

Git Branch

  • 평소 특별한 생각없이 git flow를 통해 feature 브랜치를 생성하고 사용했는데 이번에 revert를 해야하는 상황을 마주하며 브랜치 관리의 중요성을 깨달았습니다.

  • 이때 만약 브랜치를 생성하고 따로 작업을 했다면 revert할 필요 없이 기존 브랜치로 돌아가 작업하던 브랜치를 삭제하면 되는 간단한 작업이었을테지만 브랜치를 세세하게 분리하지 않으니 revert같은 명령어를 사용해야하는 상황이 생겼습니다.

  • 앞으로는 상세한 branch 관리를 통해 git을 더 잘 사용해야겠다고 깨달았습니다.

profile
개발자 지망생

0개의 댓글