[레벨2 - 미션3] 장바구니 기억에 남는 피드백

Nine·2022년 6월 20일
0

API 통신 Timeout 처리

레벨1에서도 했던 크루가 있었어요. (API 처리를 기가막히게 하시는...)

협업 때 가면 정말 중요한 것이 에러 처리라고 생각됩니다.

const request = async (url, option) => {
  // AbortController 인터페이스는 하나 이상의 웹 요청을 취소할 수 있게 해줘요. JS Web API입니다.
  const fetchController = new AbortController();
  // 오... 이렇게 옵션을 지정!
  const newOption = { ...option, signal: fetchController.signal };

  const timerID = setTimeout(() => fetchController.abort(), REQUEST_TIMEOUT);

  try {
    const response = await fetch(process.env.REACT_APP_API_URL + url, newOption);
    const jsonBody = await response.json();

    clearTimeout(timerID);

    return {
      status: response.ok ? REQUEST_STATUS.SUCCESS : REQUEST_STATUS.FAIL,
      statusCode: response.status,
      content: jsonBody,
    };
  } catch (error) {

자랑스러운 우리 크루의 에러 처리 ✋


key값으로 uuid 맞을까?

Array.from({ length: LOAD_ITEM_AMOUNT }).map(() => (
  <ItemSkeleton key={uuidv4()} />
		...
	</ItemSkeleton>

uuid는 호출할 때마다 key값이 달라집니다 → key값을 고유하게 주긴했지만 결국 달라지면 VirtualDOM에서는 판단을 제대로 못해요

삽입, 삭제가 없고 추가만 존재한다면 uuid를 호출하기보다는 이름+index조합으로 key값을 설정하는 방식도 괜찮을 것 같아요.


범용적인 Header

import StyledHeader from 'components/header/style';

const Header = ({ left, right, ...rest }) => {
  return (
    <StyledHeader {...rest}>
      {left}
      {right}
    </StyledHeader>
  );
};

export default Header;

왼쪽, 오른쪽으로 나뉜 경우에는 깔끔한 구조가 맞습니다.

하지만 그렇지 않은 경우라면 left,right를 받는 것이 맞을까요? (만약 left, right가 나뉘어진 구조가 아니라면?)

children을 받아서 받는 쪽에서 열심히 해주는 것이 더 맞을 것 같아요.


css reset 해주기

import reset from 'styled-reset';

const GlobalStyles = createGlobalStyle`
    ${reset}
    html,
    body,
    #root {
      width: 100%;
      height: 100%;
      margin: 0;
      font-family: 'Noto Sans KR';
      color: red;
    }
    input,
    textarea,
    button {
      font-family: 'Noto Sans KR';
    }
`;

export default GlobalStyles;

오오 styled-reset에서 가져와서 바로 리셋~

늘 reset을 위한 css import하거나 CDN으로 가져왔는데 정말 깔끔해!


최신값을 위한 Ref 사용

const countRef = useRef(count);
countRef.current = count;

const getCountTimeout = () => {
  setTimeout(() => {
    setTimeoutCount(countRef.current);
  }, 2000);
};

setTimeout이 클로저에 의존하기 때문에 업데이트된 state를 위해 ref를 사용하는 크루를 봤어요. (최신화된 값을 제대로 사용할 때에는 ref도 고려해보자!)

React - setTimeout function 에서 state 접근 (tistory.com)


useLayoutEffect에 API 요청?

  • 단순히 useEffect보다 먼저 실행되니 더 빠르게 데이터를 불러올 수 있을 것이라 생각했습니다.

  • 하지만 아니였습니다.

  • useLayoutEffect는 컴포넌트 렌더링 후 동기적으로 실행되어서 브라우저 paint를 블로킹하면서 웹앱을 일시중지시킵니다.

  • 빠른 데이터 fetching보다 브라우저 paint를 블로킹하면서 생기는 이슈가 더 크겠죠..


살아있는 Header

현재는 Link로 하고 있지만, NavLink를 사용해볼까요?

Active 상태를 파악할 수 있고 Style을 주입시켜줄 수 있어요.

import { Link, NavLink } from "react-router-dom";
import * as S from "./index.styles";
import ShoppingCartIcon from "../ShoppingCartIcon";
import { useTheme } from "@emotion/react";
	const Header = () => {
        </div>
      </Link>
      <S.NavContainer>
        // 👉 이렇게 NavLink를 사용하면
        <NavLink exact to="/shopping-cart">
          장바구니
        </NavLink>
        <NavLink exact to="/shopping-list">
          주문목록
        </NavLink>
      </S.NavContainer>
    </S.Header>
  );

// 👉 이렇게 active class가 붙어요.
.active {
  text-decoration: white wavy underline;
}

useInfinity 스크롤 with ref

무한 스크롤을 위한 커스텀훅을 만든 크루.. 멋져멋져✨✨

import { useCallback, useEffect, useRef } from "react";

const useInfinityScroll = (ref, cb, endPoint) => {
  const observer = useRef(null);
  const onIntersect = useCallback(
    ([entry]) => {
      if (entry.isIntersecting) {
        cb();
      }
    },
    [cb]
  );

  useEffect(() => {
    if (endPoint) {
      return observer.current && observer.current.disconnect();
    }

    if (ref.current) {
      observer.current = new IntersectionObserver(onIntersect, {
        threshold: 0.9,
      });
      observer.current.observe(ref.current);
    }

    return () => observer.current && observer.current.disconnect();
  }, [endPoint, onIntersect, ref]);
};

export default useInfinityScroll;

코드를 인용하는 경우

이번 미션에서는 Redux Thunk Middleware를 사용했었는데요,

코드가 몇 줄 되지 않아 직접 코드를 가져올 수 있었어요.

이럴 경우에는 동료, 코드를 보는 사람들을 위해 JS DOC을 잘 사용해주면 좋을 것 같아요.

/*
 * @see {@link https://github.com/reduxjs/redux-thunk}
 */

export default function createThunkMiddleware(extraArgument) {

리듀서의 초기상태

const INITIAL_STATE = {
	...
}

이렇게 상수로 처리하는건 어떨까요? 더 안전할 것 같아요.

아니면 Object.freeze를 사용한다든지!


색상 네이밍

예컨대 100이 500보다 더 진한 색인지, 반대로 더 옅은 색인지조차 알 수 없으니, 그럴거면 차라리 gray_333, gray_aaa, gray_bbb 처럼 rgb색상이라도 알 수 있게 했다면 좋았을거란 생각이에요.

혹은 색상이 총 다섯개라고 하면

gray_ligher / gray_light / gray / gray_dark / gray_darker

이런 식의 구분이 더 명확할 것 같아요.

  • 이 부분은 리뷰어님의 의견이예요.
  • 하지만 숫자로 팔레트를 지정하는 것을 권장하는 분들도 계시더라구요.

container presentation은 유물

container component vs. presentation component 구분법은 해당 개념을 처음 소개한 Dan Abramov조차 실수였음을 인정한 과거의 유물이에요.

이제는 그런 개념은 아예 잊으셔도 좋습니다.


Single source of truth

어떤건 로컬에만 담기고 어떤건 리덕스 스토어에 담으면 Single source of truth 라는 리덕스의 대원칙에 적극 반하는 결과를 낳을 수 있어요.

  1. 서버통신데이터(A) -> 전역에서 필요(B)

  2. 서버통신데이터(A) -> 지역에서 필요(C)

  3. 클라이언트데이터 (B, C)

A만 뽑아내어 별도로 관리하는 라이브러리가 있다면 (react-query, swr 등), 전역(B)에 저장할지 지역(C)에 저장할지는 개발자가 알아서 판단하면 될 문제겠죠.

redux (최근의 recoil, zustand, jotai 등)는 B만 담당하면 되는거구요.

  • 저 또한 2단계에서 리뷰님께 피드백을 받았었는데요, "data를 fetch하는 error, loading을 리덕스 스토어에서 관리할 수준의 데이터가 될까?"라는 물음을 던져주셨어요.

  • 결론적으로는 서버통신데이터를 관리하는 라이브러리를 사용하는 것이 좋다는 판단을 했고, 이번 미션에서는 외부 라이브러리를 사용하면 안 되었기 때문에 fetch관련 data, error, loading은 지역에서 처리해주었습니다.


도대체 CRA에서 종속성 차이가 먼데

https://github.com/woowacourse/react-shopping-cart/pull/116#discussion_r878990869

Dan이 devdependencies, dependencies 상관없다해도 리뷰어님은 그래도 구분하신다는 말씀!


반복문의 비동기 처리

// 정상 동작 x 
const handleClickDeleteAllButton = async () => {
  await checkedIdList.forEach((targetId) => {
    callDeleteApi(targetId);
  });
  await getCartList();
};
const handleClickDeleteAllButton = async () => {
	// promise.all을 통해 전부 비동기 처리되고 동작하도록 수정!
  await Promise.all(
    checkedIdList.map((targetId) => {
      return callDeleteApi(targetId);
    }),
  );

  await getCartList();
};

when given then

테스트를 작성하는 방법 중 하나로 when-given-then 패턴이 있는데요, 사실 공원이 초반에 설명해줬었죠!

Given When Then 패턴


확장성의 핵심

type이 더 많아질 예정일까요?

1. 바뀌지 않는 부분을 추출해서 AmountBox 라는 상위 컴포넌트 만들기
2. CartAmountBox, PayAmountBox 컴포넌트를 AmountBox를 활용해서 따로 만들기
3. 새로운 타입이 추가되면 그때마다 xxxAmountBox 만들기

'바뀌는 부분''바뀌지 않은 부분'을 분리하는 게 확장성의 핵심이라고 생각합니다.

'바뀌는 부분'과 '바뀌지 않은 부분'을 분리하는 게 확장성의 핵심


수량 추가 빼기

  • quantity없이 id만으로 백엔드에 요청하는것이 기본입니다.

두 가지 방법이 떠오르죠.

  1. 백엔드에 요청할 때 plus, minus 해달라고 요청(상태값은 백엔드에 이미 다 있음), 백엔드에서 처리가 다 끝나면 새로운 값을 응답으로 줘서 프론트의 상태를 갱신하는 방법

  2. 프론트에서 상태 갱신을 담당하고 백엔드에는 그냥 바뀐 값으로 수정만하라고 요청

게임 구현할 때는 메모리 조작하는 해커들 때문에 중요한 로직에 1번 방식 많이 씁니다.

1번은 당연하고 2번도 호출부에서 수량을 받아오지 않아도 된다고 생각하는데요.왜냐면 프론트에서 상태를 변경한다고 해도 store 내부에 이미 존재하는 수량을 활용해서 백엔드에 보내면 된다고 생각했습니다.

즉 결론적으로 두가지 방법 모두 프론트에서는 증가, 감소에 대한 요청만 필요하지 quantity까지 보내줄 필요는 없다는 것 입니다.


컴포넌트 테스트, 리듀서 테스트

컴포넌트 테스트

  • react-testing-lib랑 같이 써서 테스트를 작성해보면 어떨까요?

writing-tests#components

리듀서 테스트

  • 다른 액션들에 대해서도 어떤 상태에서 어떻게 변경 되는지 테스트해보면 좋을 거 같습니다.(아주 단순한 상태 변화는 굳이 작성 안하셔도 될 거 같은데요, 복잡한 상태나 중간에 로직이 들어가면 의미있는 테스트가 될 수 있을 것 같아요.)

writing-tests#reducers


조건부 msw return

msw? 처음 해봤다고 쫄지 맙시다. 그냥 javascript예요.

rest.get('/user', (req, res, ctx) => {
  const userId = req.url.searchParams.get('userId')
  if (userId === 'abc-123') {
    // Return a mocked response only if the `userId` query parameter
    // equals to a specific value.
    return res(
      ctx.json({
        firstName: 'John',
        lastName: 'Maverick',
      }),
    )
  }
  // Otherwise, explicitly state that you wish to perform this request as-is.
  return req.passthrough()
})

Response resolver - Basics - Mock Service Worker Docs (mswjs.io)

profile
함께 웃어야 행복한 개발자 장호영입니다😃

0개의 댓글