[2022 HUFS 하계 모각코] 0806 TIL

KwakKwakKwak·2022년 8월 6일
0
post-thumbnail

Context를 하나로 두지 않고 찜 리스트와 구분되는 전체 메뉴 데이터를 장관하는 MenuContext를 따로 만들어

메뉴 데이터를 불러오고 페이지가 바뀔 때마다 한꺼번에 관리해주는 'MenuContext'와
메뉴를 클릭했을 때 찜 리스트(?)로 넘겨주고 체크박스, 찜 삭제를 관장하는 'Context',
총 2개의 Context를 관리하고자 했다. 이러한 아이디어는 아래 벨로퍼트님 글을 읽고 영감을 받았다.

Context가 꼭 전역적이어야 한다는 생각을 버리자

Context를 2개로 분리하고 나니, 각각의 Context가 무엇을 하는지 한 눈에 딱 보이게 정리됐고, 딱 변수명에 걸맞는 추상화가 이루어져 아주 만족스러웠다.

그러나 문제가 발생했는데,

지금 이 프로젝트의 구조도는 '마이페이지'와 마이페이지를 제외한 카테고리 페이지 2개로 나뉘어져 있지만 사용하는 데이터만 다를 뿐 페이지의 작동 원리와 템플릿이 동일하다는 모순점이 존재했다.

근디 이 구조를 Context 설계 당시엔 몰랐기 때문에 Context가 제공하는 함수들이 다르게 만들어졌다. MenuContext 안의 loadPage 함수는 Pagination에 공통으로 필요한 함수이지만 함수가 강하게 MenuContext와 결합돼있어 재사용이 불가능했다.

물론 고작 함수 하나여서 다시 만들어도 상관 없었겠지만, 코드의 비효율성은 이런 자잘한 것부터 최대한 줄여야하기 때문에 기록하려고 캡쳐하였다. 이런게 경험치 차이고 아만보(아는 만큼 보인다는 뜻 ㅎ)인 것이다.


또한 Pagination, MenuTemplate, Menu 각각의 컴포넌트에서 직접 MenuContext 값들을 불러와 사용했던 것을 props로 넘겨받아 좀 더 재사용률이 높고 범용성 있도록 개선하려 했다.

근데 또 문제 발생... props로 컴포넌트 별 필요한 value와 함수들을 넘겨주는 건 Context를 사용하는 의미가 없어지게 되고, 무엇보다도 Mypage와 Mypage 외의 페이지들이 완벽하게 이분법적으로 나뉘지 않아(Menu 컴포넌트는 전적으로 Context.js 자원만을 사용함) Context가 겹치는 문제가 발생한 것이다..

이 문제의 원인을 리액트 공식문서에서 찾았다.

Context 객체를 구독하고 있는 컴포넌트를 렌더링할 때 React는 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider로부터 현재값을 읽습니다. ... Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시됩니다.

현재 내 App.js를 살펴보면, Context의 Provider 아래 MenuContext의 MenuProvider로 Provider가 두 겹 겹쳐져 있다. 따라서 Menu 컴포넌트가 Context의 Provider를 참조하고 싶어도 가장 가까운 Provider인 MenuContext의 Provider에서만 그 값을 읽으려 시도하기 때문에 오류가 발생했던 것이다.

그리고 Menu 컴포넌트에서는 Context에서 사용할 데이터의 갯수도 일치하지 않아 캡슐화가 더 어려워지는 문제가 발생했다.

온갖 방법을 시도해서 2개 파일의 Context를 각각 유지하고자 했으나 해결할 수 없었다. 아마 Context파일을 2개 이상 만드는 시도에 대한 검색 결과를 찾을 수 없는 걸 보면 내 시도의 전제가 틀렸다는게 아닌가 하는 생각이 든다.

API를 통해 불러온 데이터를 1차 가공해서 음식 카테고리 페이지마다 나열해놓는 1차 처리 작업을 Context API로 가져가서 전역적으로 한 번에 관리하고자 한 것인데(한 페이지에 8개 메뉴를 display하는 데이터 슬라이싱과 pagination 기능을 한 번에 처리하고자 함), 결과는 성공적이었으나 알 수 없는 이유로 상위 Provider value를 읽어오지 못하는 에러를 맞닥뜨리게 됐다.

그래서 MenuContext와 Context를 하나로 병합하고 정리해줬는데도 똑같은 에러가 Menu 컴포넌트에서 발생했다. 혹시나해서 Menu 컴포넌트 안에서 사용하고 있는 useCartActions 훅 대신 다른 훅을 사용하도록 바꿔봤다. 그랬더니...

잘 돌아간다.. 그렇다면 useCartActions 훅이 문제였다는 것. CartActionsContext의 Value인 cartActions 객체를 살펴봤다.

const cartActions = useMemo(
    () => (
      {
        add(item) {
          const id = idRef.current;
          idRef.current += 1;
          setSelected((prev) => [...prev, { id, ...item }]);
        },
        toggle(id) {
          setSelected((prev) =>
            prev.map((item) => {
              return item.id === id ? { ...item, done: !item.done } : item;
            })
          );
        },
        remove(id) {
          setSelected((prev) => prev.filter((item) => item.id !== id));
        },
        loadPage(num) {
          setNowPage(num);
          setPageItems(selected.slice(offset(num), offset(num) + limit));
        },
      },
      setPageItems(selected.slice(offset(nowPage), offset(nowPage) + limit))
    ),
    [nowPage]
  );

useMemo로 불필요한 렌더링을 막아주고 nowPage가 업데이트될 때마다 갱신되도록 해놨다. add, toggel, remove, loadPage 함수 외에 찜 목록에 담긴 pageItems를 8개 offset 단위로 잘라주는 setPageItems 함수를 기본적으로 실행되도록 바깥으로 빼놨었다. 근데 이 코드를 지워주니 cartActions가 정상적으로 작동했다. 왜 그런 것일까?

cartActions의 구조를 다시 살펴보면,

const cartActions = useMemo(
	() => (
    	{...obj}
    )
)

useMemo를 벗겨내면 기본적으로 화살표 함수의 형태를 띄고 있다. 화살표 함수 안에는 객체의 형태를 띄고 있다. 결과적으로 cartActions는 객체{}를 반환하는 함수인 것이다.

화살표 함수

화살표 함수는 본문이 한 줄인 함수를 작성할 때 유용합니다. 본문이 한 줄이 아니라면 다른 방법으로 화살표 함수를 작성해야 합니다.

  • 중괄호 없이 작성: (...args) => expression – 화살표 오른쪽에 표현식을 둡니다. 함수는 이 표현식을 평가하고, 평가 결과를 반환합니다.
  • 중괄호와 함께 작성: (...args) => { body } – 본문이 여러 줄로 구성되었다면 중괄호를 사용해야 합니다. 다만, 이 경우는 반드시 return 지시자를 사용해 반환 값을 명기해 주어야 합니다.

화살표 함수의 기본 모양은 () => {} 형태이다. 소괄호 안에 매개변수가 입력되고, 중괄호 안에 return될 값이 들어가는 형태이다.

여기서 소/중괄호가 생략될 수 있는 조건은 괄호 안의 내용물이 단 하나일 때이다. 매개변수가 하나이거나, return될 값이 한 줄의 코드라면 괄호들은 생략해도 무방하다.

기본적으로 중괄호는 여러 줄의 코드가 포함될 때를 대비하는 역할이다.

const a = () => {
	...
    return result
}

만약 중괄호 안의 코드가 한 줄이라면 굳이 () => { return {a: 1} } 이 아니라 () => ({a: 1}) 로 축약해서 적는 것이 좋다. 여기서 중괄호와 return은 지워졌는데 소괄호로 감싸져있는 이유는 반환되는 값이 객체이기 때문이다.

여기까지 알고 난 뒤 내 cartActions 객체를 보면 괴이함을 느끼게 된다.

() => ({함수 객체}, 함수) 도대체 무엇을 return하는 것인가?

그래서 삐쭉 튀어나와있는 setPageItems 함수를 지우자마자 잘 실행됐던 것이다. 함수의 타입과 상관 없이 현재 nowPage값에 맞는 selected 페이지를 설정하기 위한 setPageItems 코드는 그럼 어떻게 처리해줘야할까?

const cartActions = useMemo(
    () => ({
      add(item) {
        const id = idRef.current;
        idRef.current += 1;
        setSelected((prev) => [...prev, { id, ...item }]);
        setPageItems(selected.slice(offset(nowPage), offset(nowPage) + limit));
        calculatePages(selected);
      },
      toggle(id) {
        setSelected((prev) =>
          prev.map((item) => {
            return item.id === id ? { ...item, done: !item.done } : item;
          })
        );
      },
      remove(id) {
        setSelected((prev) => prev.filter((item) => item.id !== id));
      },
      loadPage(num) {
        setNowPage(num);
        setPageItems(selected.slice(offset(num), offset(num) + limit));
      },
    }),
    []
  );

0개의 댓글