"Life is Full of Possibilities" - Soul, 2020

우아한테크코스

레벨 2 회고 - React를 '잘' 활용해 보자!

m2ndy 2024. 6. 16. 22:43

 
 
 
리액트에 대해 잘 안다고 생각했던 내가 무지했었음을 깨닫고
처음부터 차근차근히 배워갔던 레벨 2였다.
 

레벨 2에 배운 내용들을 백지에 써보기!


 

미션 1. 페이먼츠

 
- Github : https://github.com/chosim-dvlpr/react-payments/tree/step2
- 배포 주소 : https://chosim-dvlpr.github.io/react-payments/dist/
- Storybook : https://6620c28ba5e20036aa444298-eyqndbcamz.chromatic.com/
 
 
 
[학습 목표]
- 재사용 가능한 컴포넌트 만들기
- Storybook 활용
- 구성 요소들 간의 효율적인 상태 관리
- Custom hook 적용하여 Form 관리 로직을 분리하고 재사용
- Controlled & Uncontrolled components에 입각하여 Form 핸들링
 
 
이번 미션에서는 여러 종류의 input을 만들고, 이에 대한 유효성 검사를 처리하는 로직을 작성했다.
 
강의에서 준이 설명해 준 "우리가 무엇을 하려고 하는 거지?"라는 생각을 기반으로 앱의 핵심 기능을 작성해 보았을 때 다음과 같은 결론이 나왔다.
 
사용자 입력을 실시간으로 검증한 뒤 카드 미리 보기를 동적으로 업데이트한다!
 
 
동작 가능한 가장 작은 버전의 핵심 사이클인 [입력 - 유효성 검사 - 출력] 과정을 가장 먼저 구현하려고 했다.
input에서 사용자의 값을 받아오고, 유효성 검사 후 CardImage 컴포넌트로 넘겨 기능이 잘 작동되는지를 확인했다.
이 과정에서 유효성 검사의 로직 수정, input type 지정 등이 필요하다는 피드백을 얻을 수 있었고, 테스트 도구 없이도 TDD적 사고방식이 어떤 것인지를 체감할 수 있었다.
 
 
 
[컴포넌트 분리 및 재사용]
앱에서 input 컴포넌트가 다양한 곳에 쓰이는데, (카드 번호, 유효 기간, 소유자 이름, CVC, 비밀번호) 이를 'Input'이라는 컴포넌트 하나만으로 구현하기 위해 애를 먹었던 기억이 난다. 타입이 카드 번호나 CVC, 비밀번호인 경우 max-length를 지정하고, 유효성 검사를 통과하지 못해 error인 input일 경우 css컬러도 지정해 주었다.
추가로, 작은 단위의 컴포넌트는 도메인을 모를수록 재사용이 수월해진다는 것을 알 수 있었다. 따라서 Input 컴포넌트를 최대한 html input 태그에 가깝게 구현했고, 필요한 값들은 props를 받는 형태로 작성했다. 확실히 재사용이 편리해짐을 느낄 수 있었다.
 
[상태 관리]
state의 경우 상단에서 하위 컴포넌트로 뿌려주는 방식을 사용했다. 하위 컴포넌트에서 업데이트된 값을 다른 하위 컴포넌트에서 사용해야 했기 때문이다. 그리고 아직 Context API와 같은 전역 상태 관리를 사용하는 단계가 아니라서, 상태는 모두 props로 전달했다.
어떻게 해야 다양한 상태들을 효과적으로 관리할 수 있을지 고민을 많이 했었다. 유효기간의 상태를 month와 year로 바꿨다가, 객체로 바꿨다가 .. 여러 방법들을 시도해 봤다. 결국 각 상태를 객체로 만들어 data와 errorMessage를 저장하고, data 내부에 배열 또는 객체로 input값들을 관리했다. 
 
[Storybook]
이번 미션을 통해 스토리북을 처음 사용해 봤다. 사용하고 나서 느낀 장점은 '빠른 피드백'을 받을 수 있다는 것이다. TDD와 마찬가지로, 스토리북을 통해 실시간으로 피드백을 받을 수 있기 때문에 어느 부분에서 수정이 필요한지, 어디에 인자가 필요한지 알 수 있었다.
또한, 스토리북 내에 props의 type과 description 등을 명시함으로써 컴포넌트 별 문서화가 가능하고, 이를 바탕으로 다른 부서의 사람들과 효율적인 의사소통이 가능하다. 추후 프로젝트 때 사용하면 좋을 것 같다!
+ Chromatic을 활용하여 Storybook 자동배포도 구현했다
 
[Emotion]
늘 사용하던 styled-components 라이브러리 대신, css 라이브러리로 emotion을 사용했다. emotion의 번들 사이즈가 styled-components보다 작고, 의존성 개수가 더 적기 때문이다. emotion 패키지 styled와 react 중 react를 사용했는데, styled 패키지는 styled-components와 매우 유사하기 때문에 새로운 것을 배워보기 위해 react 패키지를 사용했다. 

>> 사용 이유 및 느낀 점에 대해 자세히 보고 싶다면?
https://github.com/woowacourse/react-payments/pull/340#issuecomment-2068036011
 
 
 
 

미션 2. 모듈

 
- Github : https://github.com/chosim-dvlpr/react-modules/tree/refactor
- Storybook : https://663339f504de8c832eb86c8e-romtuuafsx.chromatic.com/?path=/story/modal--default
- Modal 라이브러리 배포 : https://www.npmjs.com/package/woowacourse-react-modal-component
- 커스텀 훅 라이브러리 배포 : https://www.npmjs.com/package/woowacourse-card-custom-hook
 
 
[학습 목표]
- 재사용 가능한 모듈화 된 컴포넌트 개발 및 npm 배포
- 요구사항 변경에 따른 컴포넌트 리팩터링 및 개선
- 모듈 npm 배포 (모달, 커스텀 훅)
- Storybook과 RTL 활용한 컴포넌트 문서화 및 테스트 시나리오 작성
- 합성 컴포넌트 적용
 
npm 배포라니..!
이번 미션 덕분에 꿈꿔왔던 npm 배포를 할 수 있게 되었다.
 
 
[npm 배포, RTL]
총 2개의 배포를 진행했다.
 
1. 모달 컴포넌트
- children을 받아 사용자가 원하는 컴포넌트를 주입할 수 있는 라이브러리
 
2. 카드 입력 로직 관련 커스텀 훅
- 카드 번호, 카드사, CVC, 만료 기간, 비밀번호, 카드 소유자 입력 및 유효성 검사를 할 수 있는 라이브러리
 
미션 1단계에 이어 2단계에서는 기존 기능을 확장했는데, 모달 컴포넌트의 경우 다양한 크기와 종류에 대응해야 했다.
따라서 모달 컴포넌트는 합성 컴포넌트를 적용하여 Header, Subtitle, input, button을 사용자가 자유롭게 사용하는 방안으로 기능 확장 및 리팩터링을 진행했다.
 
커스텀 훅의 경우에도 visa, mastercard 외에도 여러 카드사에 대응할 수 있어야 했다. 기존 커스텀 훅에서 유효성 검사 로직을 추가하여 확장했다.
사실 커스텀 훅 2단계에서는 테스트에 집중했던 것 같다. 사용자의 사용 흐름을 고민하고 RTL의 test.each와 act, renderHook을 활용하여 테스트 코드를 열심히 작성했다.

 
 
 
 
 
 

미션 3. 장바구니

 
- Github : https://github.com/chosim-dvlpr/react-shopping-cart/tree/step2
- api 서버 링크가 http라 CORS 이슈가 있어 배포를 진행하지 않았다. - 그냥 일단 해볼까?
 
 
[학습 목표]
- Recoil 활용한 전역 상태 관리 (클라이언트 상태)
- 복잡한 파생 상태 관리
- RTL 활용한 핵심 기능 테스트
 
 
이번 미션부터는 서버와 자원을 주고받으며 상태를 관리해야 했다.
컴포넌트가 복잡해지고, 이에 따라 전역 상태 관리 라이브러리인 Recoil을 적용하게 되었다.
 
 
[Recoil]
Atom과 Atom Family로 장바구니 아이템( itemsState )과 수량 정보( itemDetailsState )를 관리했다. 서버 상태는 itemsState에서 관리하고, 클라이언트 상태는 itemDetailsState에서 관리했는데, 지금 다시 생각해 보면 장바구니 아이템을 Recoil에 받아 관리를 해야 했을까 라는 의문이 든다. 서버 상태를 그대로 화면에 보여주는 것이 관리해야 할 atom의 수량도 줄고 테스트해야 하는 코드도 줄고 더 효율적이지 않았을까 싶다.
 
[다양한 조건 처리 - 쿠폰]
대망의 쿠폰... 역대급으로 어려웠던 로직이다. 장바구니에서 사용자가 체크한 상품을 넘기고, 총 결제 금액에서 할인 금액을 계산했다. 이에 따라 couponState, selectedCouponState가 추가되었다. selected 상태가 따로 존재하는 이유는, 모달에서 쿠폰을 선택하더라도 '적용하기' 버튼을 누르지 않으면 실제 할인 금액에 포함되지 않는 것으로 구현했기 때문이다. 어떤 쿠폰을 선택했을 때 최대 할인 금액을 받을 수 있는지, 추가 주문을 하게 될 때 최대 할인을 받을 수 있는지 쿠폰을 눌러보며 확인할 수도 있기 때문이다. 또한, 최대 쿠폰 적용 개수를 넘게 되면 UX를 고려하여 다른 쿠폰은 disabled 상태로 두었다.
추가로 적용하고 싶은 부분은 사용 가능한 쿠폰만 정렬하여 상단에 보여주는 것인데, 만약 쿠폰이 100개로 많아진다면 스크롤을 내리며 쿠폰을 찾기 어려울 것이다. 이때 사용 가능한 쿠폰이 가장 위쪽에 있다면 사용자는 스크롤을 많이 내리지 않아도 편하게 확인할 수 있어서 서비스를 사용하기 편리해지겠지.
 
[커스텀 훅 vs Selector]
커스텀 훅을 심화하여 쿠폰이 유효한지, 적용 가능한지, 찾고자 하는 쿠폰이 있는지.. 등등 쿠폰 로직과 관련된 훅을 작성했다. 이 과정에서 커스텀 훅과 Selector의 역할이 모호하다는 생각이 들었다.
먼저 Selector는 순수함수여야 하고, Recoil에서 나온 것이기 때문에 비동기 처리를 할 수 없다는 특징이 있다. 반면 커스텀 훅은 리액트 훅으로 동작하기 때문에 비동기 처리가 가능하여, Selector의 역할보다 더 넓은 역할을 할 수 있다고 생각했다.
이 부분에 대하여 리뷰어에게 질문했고, Selector는 상태 변경 시 해당 부분만 재계산 + 캐싱에 유리, 파생 데이터 계산에 사용, 커스텀 훅은 재사용할 수 있는 로직에 사용된다는 답변을 받았다.
 
>> 관련하여 미션에서 작성했던 PR을 보고 싶다면?
https://github.com/woowacourse/react-shopping-cart/pull/318#issuecomment-2136417142
 
 
[Suspense, ErrorBoundary]
장바구니 목록을 서버에서 받아오는 동안 Suspense를 걸어주고, 컴포넌트의 에러를 감지하는 ErrorBoundary를 적용했다. ErrorBoundary를 처음 접했는데, 리액트의 경우 에러가 발생했을 때 앱 전체를 멈추어 흰 화면만 보이기 때문에 ErrorBoundary를 통해 fallback component를 보여주는 것이 UI, UX적으로 좋다고 한다. 단, 이벤트 핸들러나 비동기 코드, SSR 내에서는 에러를 잡아낼 수 없기 때문에 적절한 조치가 필요하다고 한다.
 
- Cart 페이지 코드
https://github.com/chosim-dvlpr/react-shopping-cart/blob/step2/src/pages/Cart/Cart.tsx

function Cart() {
  return (
    <S.CartContainer>
      <Header headerIconType="home" />
      <ErrorBoundary FallbackComponent={ErrorFallback}>
        <S.ContentWrapper>
          <Suspense
            fallback={<S.SuspenseFallBack>Loading...</S.SuspenseFallBack>}
          >
            <CartContent />
          </Suspense>
        </S.ContentWrapper>
        <Footer message={MESSAGES.confirm} isDisabled={false} url={URL_PATH.order} />
      </ErrorBoundary>
    </S.CartContainer>
  );
}

 
 
 
 

미션 4. 상품 목록

 
- Github : https://github.com/chosim-dvlpr/react-shopping-products/tree/step2
- 배포 주소 : https://react-shopping-products-three.vercel.app/ - 안전하지 않은 콘텐츠 허용으로 설정해야 보인다!
 
 
[학습 목표]
- MSW 사용하여 API 요청 모킹
- 비동기 작업의 상태 관리, 적절한 UI 렌더
- RTL 비동기 테스트
- React Query 사용한 서버 상태 관리
- API 연동 과정 중 발생하는 다양한 에러 상황에 대응
 
 
상품 목록 미션에서는 TDD로 구현했다. 테스트 코드를 작성하며 어떤 방식으로 구현할지 그려나가고, 이를 바탕으로 핵심 코드를 작성했다. MSW와 비동기라는 큰 벽이 있었지만....몇몇 테스트 케이스를 제외하고는 바로 구현할 수 있었다.
 
 
[React Query]
이전 미션에서 다뤘던 서버 상태와 클라이언트 상태 중, 서버 상태를 다루기 위한 도구로 React Query를 적용했다. React Query를 사용함으로써 서버와의 데이터 동기화를 통해 정보를 보여줄 수 있게 되었다. 낙관적 업데이트 멈춰! 더불어, 리액트 쿼리의 자체 기능인 캐싱을 적용하여 api를 호출하는 횟수를 줄일 수 있게 되었다. 서버 부담을 줄이고 데이터 통신이 느린 지역에서도 캐싱 데이터를 활용해 렌더 속도를 향상할 수 있다.
이때 캐싱 전략이 필요했는데, 데이터 변경 주기와 캐시 무효화 전략이 필요했다.
먼저, 상품 목록을 보여주는 페이지는 데이터가 실시간으로 변할 필요가 없기 때문에 캐싱 시간을 길게 설정했다. 만약 상품 목록이 아니라 이벤트 페이지, 주식이었다면 실시간 데이터가 중요해서 캐싱 시간이 매우 짧아야 할 것이다.
단순히 GET 요청으로 서버 데이터를 받아오는 액션일 경우 기존 캐시를 갱신하는 것이지만, useMutation을 활용하여 POST, DELETE, PATCH 등의 요청으로 서버 상태를 변경하는 액션은 관련 캐시를 무효화하고 새로운 데이터를 받아와야 한다. 따라서 invalidateQuerie 메서드를 적용했다.
 
참고로 좋아요와 댓글은 실시간 반응성이 중요해서 낙관적 업데이트를 하지만, 장바구니 담기나 수량 업데이트 관련 로직은 서버와 동기화를 시켜준다고 한다. 중요한 도메인 로직이기 때문에 이렇게 한다고 한다.
 

 
[Context API]
리액트 쿼리가 요구사항에 없던 step1에서는 장바구니 상품들의 id값을 cartIdSet이라는 상태로 관리했고 Context API를 통해 useFetchAddcart 훅을 전역으로 전달했다. 하지만 리액트 쿼리를 도입하면서 Context의 쓸모가 없어졌고, 장바구니 상품 id값을 서버 상태 그대로 받아와 관리하는 방식을 적용했다.

export const CartContext = createContext<ReturnType<typeof useFetchAddCart>>({
  cartIdSet: new Set(),
  setCartIdSet: () => {},
  postToAddCart: async () => {},
  deleteToRemoveCart: async () => {},
  fetchCart: async () => [],
});

 
 
[MSW]
대망의 MSW.... 비동기 테스팅을 적용할 때 가장 힘들었다. await waitFor의 여부에 따라 결과값이 크게 달라지고, msw 핸들러에 의해서도 값이 달라질 수 있기 때문이다.
테스트 중 끝까지 통과가 되지 않다가 미션 제출 전날 원인을 해결한 것이 있었다. useFetchAddCart 훅에서 '장바구니에 담겨있지 않은 제품을 담으면 장바구니에 담긴 제품 종류 개수가 증가되어야 한다'라는 테스트였는데, mock data에서 문제가 발생했던 것이었다. 장바구니에 상품을 담는 요청을 보내면, msw 핸들러가 mock data에 상품을 담는 로직이다. 그런데 테스트마다 같은 mock data를 사용한 뒤 초기화를 하지 않았고 장바구니에 상품이 중첩되어 쌓이는 결과를 낳았다. 따라서 beforeEach를 통해 mock data를 초기화하는 로직을 추가했다.
 
 
 

레벨 2 글쓰기

"어떻게 살아갈 것인가"를 주제로 작성했다. 혼란스러웠던 레벨 2 동안의 고민거리를 담아봤다.
https://github.com/chosim-dvlpr/woowa-writing/blob/Level2/Level2.md
 
 

메타인지 말하기

내가 무엇을 알고 무엇을 모르는지 인지할 수 있도록 도와주는 활동. 지난 레벨 1에 이어 레벨 2에서도 신청했다. 주제는 합성 컴포넌트, 커스텀 훅과 selector, 리액트 쿼리의 캐싱을 준비했다. 이 활동 덕분에 다른 크루들이 미션 중 공부했던 내용을 알 수 있었고, 나도 공부했던 내용을 정리할 수 있었던 시간이었다.
 
 

미니 테코톡 스터디

레벨 1부터 운영해 오던 미니 테코톡 스터디를 레벨 2에서도 이어왔다.
레벨 1에서는 이벤트 버블링, 스코프와 같이 javascript를 베이스로 공부했다면, 레벨 2에서는 비동기 프로그래밍, prop drilling, useMemo와 useCallback, CSRF와 XSS공격까지 다뤘다. 바쁜 와중에도 밤새어가며 발표를 준비해 준 스터디원들... 너무 감사하다🥹
 
 

테코톡!

추가로 이번 미션에서 테코톡을 하게 되었다. 주제는 prop drilling! 범위가 넓어 제한 시간 내에 깊은 내용을 다루지는 못했고... 자료는 여전히 부족한 것 같고.... 긴장을 많이 해서 실수도 해서 만족스럽지는 않지만..... 어찌 됐든 테코톡이 끝났다! 우테코를 수료하려면 테코톡을 해야 한다는 강박감에 어딘가 마음 한 구석이 불편했는데, 이번 레벨에서 불안감을 마주하고 이겨낼 수 있었다. 야호!
공개하기 쑥쓰럽지만...링크는 여기! 
 
 

레벨 3 목표

1. 나를 대표할 수 있는 프로젝트 만들기
2. 새로운 기술 도전하기 (프로젝트에 따라 달라질 것 같다!)
3. 이것저것 많이 실험하고 코드에 적용하기
4. 운동 꾸준히 하기
5. 영어 회화 꾸준히 하기
6. 알고리즘 꾸준히 하기
 
 
 
다음 레벨도 잘할 나니깐!!! 아쟈쟛🍀✨