React 상태관리에 대해서 깊게 생각해보기

버들·2023년 9월 14일
0

✨Today I Learn (TIL)

목록 보기
47/62
post-thumbnail

상태관리에 대해서 고민해보게 되었다

이전에 회사에서 같은 모양 및 비슷한 형식의 input, button 등이 피그마로 만들어져 있길래, custom hook 같이 제작하고 싶어서 컴포넌트를 만들게 되었다.
물론 그때 당시 custom hook 이 기능적인 부분이 주 라는 것을 모르고, 정작 디자인을 편하고 빠르게 사용하기 위한 컴포넌트로 만들었던 것이다. :(

아무튼 그렇게 해서 제작을 하였는데, 어느 파트에서는 해당 페이지에서 사용되는 state 값이 만들어놓은 input 컴포넌트에 할당해야되는 경우가 발생하게 되었다.

여기서 원래 같았다면 평소처럼 아래와 같은 생각으로 state를 처리했을 것이다.

아 이 페이지에서만 사용되는 state 값이니, 다른 컴포넌트와의 거리가 멀어도 이 페이지 컴포넌트에서 관리를 하면 더 가독성이 좋아지지 않을까?

그런데 혼자서 개발을 진행하다보니까 내가 짜는 코드가 추후에 서비스 배포시에 문제가 없는 코드인지 점점 내 자신에 의문을 가지게 되면서 찾아보게 되는 시기가 쭉 이어졌는데, 그때 React 상태관리에 대해서 좀 더 자세히 알아보게 되었다.

React State

react에서 state는 컴포넌트 안에서 관리되고 경우에 따라 바뀌는 동적인 데이터이다. 또한 만약 자식 컴포넌트에 값들이 전해져야하는 상황이 생길 수도 있으니 state를 기반으로 동작되는 모든 컴포넌트의 상위 컴포넌트에 위치해야되며, 그것을 리액트는 추천한다.

React 상태의 역할

여기서 상태는 왜 관리하는 것이며 어떠한 역할을 하길래 중요할까 라는 생각도 한번쯤은 해볼 수도 있을 것이다.
상태는 일단 대체적으로 무언가의 변화를 감지하기 위함이다. 불변성이라고도 많이 불리는데, 무언가 값이 있다면 이 값이 기존에 default로 존재했던 값과 어떻게 달라지고 그에 따라서 어떤 영향을 주는지도 상태관리를 통해 볼 수 있다.

우리는 이 상태들을 통해서 서버에서 데이터를 가져와 그것을 캐싱 데이터로, UI와 같은 인터렉티브한 요소들을 관리 등을 할 수 있게 된다.

전역상태관리와 props drilling

또한 상태는 범위가 크게 다음과 같이 나뉘게 되는데 이 부분이 상태관리의 메인이지 않나 싶다.

  • 범위
    • 전역상태 : 전역으로 상태들을 공유하여 영향을 줄 수 있는 상태
    • 지역상태 : 깊이가 비슷한 컴포넌트들에게만 영향을 주는 상태

여기서 지역상태와 전역상태에 따라서 코드 작성 방식도 크게 달라지게 된다.

지역상태라면 부모 컴포넌트에서 나온 데이터들과의 deps (깊이) 가 깊지 않은 자식 컴포넌트에 데이터를 전달할 상황들이 오게된다. 이때 props 를 활용하여 하위 컴포넌트에게 데이터를 전달하게 되는데, 너무 깊이가 큰 컴포넌트에는 중간에 관계없는 컴포넌트에도 데이터를 전달해야할 일도 많고, 불필요한 코드 작성들이 늘어나는 props drilling 현상들도 생기게 되었다.

그래서 이를 타파하기 위함이 전역상태 라이브러리가 나오게 되었고, 하나의 store에 데이터를 저장만 한다면 어느 컴포넌트든 해당 상태를 공유 할 수 있게 되었다.
그 예로는 Redux, Mobx, Recoil, Zustand 등이 있다.

아래의 이미지는 Redux의 Flux 상태관리 구조이다.

react에서 좋은 상태관리란?

State는 관련 컴포넌트들과 최대한 가까이 배치하라

이 부분이 해당 포스트를 쓰게 된 가장 강한 요인이었다.
회사에서 회원가입 페이지를 만들게 되는데, 여러 섹션들이 있었고 이것을 하나의 페이지 내에서 컴포넌트 단위로 페이지를 구성하는 형식으로 개발하게 되었다. 그 우리 MBTI 검사할 때 하나의 페이지에서 10개씩 지문 고르고 다음 누르면 지문표만 다른 걸로 렌더링 되는 걸 생각하면 편하다.

대애애충 이런 형식인데 아래의 상태들의 자세한 정보는 대외비임으로 생략하도록 하겠다.

const SignUp = () => {
  const page = useRecoilValue(pageNumberAtom);
  const creatorId = useRecoilValue(creatorIdAtom);
  const recommendCost = useRecoilValue(recommendCostAtom);
  const name = useRecoilValue(nameAtom);
  const phone = useRecoilValue(phoneAtom);
  const adCategory = useRecoilValue(adCategoryAtom);
  const adContents = useRecoilValue(adContentsAtom);
  const adCost = useRecoilValue(adCostAtom);
  console.log(
    recommendCost,
    creatorId,
    name,
    phone,
    adCategory,
    adContents,
    adCost
  );

  return (
    <FormTemplate>
      {page == 4 ? (
        <AdCostPage />
      ) : page == 3 ? (
        <AdContentsSettingPage />
      ) : page == 2 ? (
        <CategoryPage />
      ) : (
        <NameAndPhoneNumberPage />
      )}
    </FormTemplate>
  );
};

export default SignUp;

export const getStaticProps = async () => {
  return {
    props: {
      layout: "SignUpLayout",
    },
  };
};

const FormTemplate = styled.form`
  width: 100%;
  height: calc(100vh - 120px);
  margin: 0 auto;
  padding: 2rem 1.25rem;
  position: relative;
`;

그래서 이 구조는 signup/index.tsx 파일인데, 말그대로 회원가입 라우팅을 담당하는 컴포넌트이고 page 라는 상태값에 따라서 섹션들을 보여주는 구조이다.

원래같았으면 회원가입이니 form 태그내에서 모든 요소에 이벤트 등을 걸고 submit을 했어야했는데, 이러한 방식을 활용하게 되면 form안의 요소들도 계속 props drilling 으로 관리를 해야될 것 같아 전역상태 라이브러리를 활용하였다.

그런데 제작 당시에는 문득 이런 생각이 들었다.
컴포넌트들이 모여있는 것이 있는 것이 페이지니까, 이 페이지 컴포넌트에 한번에 각 컴포넌트들이 다루는 State 값들 다루고, 내려주는게 깔끔하지 않을까?

왜 지금와서 생각해보면 이런 생각을 했는지 모르겠지만, 나름 혼자서 프론트엔드 파트를 맡는 사람으로써 내 코드에 대해 계속해서 의구심을 가졌기에 이랬던 것 같다.

우선 리액트에서는 이렇게 말한다.

"State Colocation will make your react app faster"

즉 관련 컴포넌트에는 이를 사용하는 상태와 밀접하게 배치되는 것이 좋다라는 얘기다.
만약 State가 위에서 내가 생각했던 대로 컴포넌트와의 거리가 멀어지면서 props drilling을 이루어내게되고, 그렇게 되면 이 상태가 작용을 바라는 컴포넌트에 다다르기 전까지의 사이의 컴포넌트의 리렌더링을 야기할 수 있다고 한다.
또한, 추후에 이러한 코드들 때문에 서로 관련이 없는 컴포넌트들의 상태가 한번에 관리, 즉 결합도가 높아져서 추후에 스케일 업 될 수록 사이드 이펙트의 가능성도 높아지게 된다.

그렇다면 이런 말이 나올 수 있는데..

"어? 그럼 전부 그냥 전역상태로 돌리면 가독성도, 사이드 이펙트도 해결할 수 있지 않나연?"

그렇지 않다!
전역상태를 사용하는 시점부터 이 전역으로 관리되는 State 값이 영향을 받는지를 잘 살펴야지 불필요한 리렌더링을 방지할 수 있다.
또한 전역으로 관리되어있는 state를 건들인 후에 다시 그 작업을 행하기 위해 다시 뒤로 돌아가야할 일이 생길 수도 있다.
예를 들면 지금 위의 내가 만든 회원가입 폼의 정보들을 전역으로 관리했다는 것처럼.

사실 이렇게 뭔가 개인적인 정보를 관리하기에는 전역상태관리는 정말이지 비추이긴하다. 항상 너무 불안하다! 값이 입력한데로 잘 들어갔는지...
위의 회원가입 기능을 작성할 때에는 subheader에 뒤로가기 버튼이 있었다.

export const BackButtonSubHeader = () => {
  const [page, setPage] = useRecoilState(pageNumberAtom);
  const resetAdCategory = useResetRecoilState(adCategoryAtom);
  const resetAdContents = useResetRecoilState(adContentsAtom);
  const resetAdCost = useResetRecoilState(adCostAtom);
  const resetRecommendCost = useResetRecoilState(recommendCostAtom);

  const pageDownHandler = () => {
    if (page == 3) {
      resetAdCategory();
    }

    if (page == 4) {
      resetAdContents();
      resetAdCost();
      resetRecommendCost();
    }
    setPage(page - 1);
  };

  return (
    <BackButtonSubHeaderComponent>
      {page == 1 ? null : (
        <BackButton
          src="/icon/Arrow.svg"
          alt="arrow"
          width={19.2}
          height={19.2}
          onClick={() => pageDownHandler()}
        />
      )}
      <span>{page < 3 ? "기본 정보" : "비즈니스 정보"}</span>
    </BackButtonSubHeaderComponent>
  );
};

이런식으로 Subheader 또한 레이아웃으로 전체 app를 children props로 감싸서 관리를 하고 있었기에, props drilling을 우려하여 page state 값을 전역으로 관리를 하였으며 BackButton을 누름으로써 이전 페이지를 갈 수 있게 제작을 해놓았다.

그런데 여기서의 문제가 뭐냐면, 예를 들어 유저가 page == 3 에서 무언가 정보를 입력했다가, 아 이전 페이지에서 정보를 잘못 입력한 것 같아..! 해가지고 page == 2에서 다시 정보를 수정하고, 다시 page == 3 으로 돌아오는 코스를 밟았다고 치자.

그런데 이미 이전에 page == 3 에서 정보를 입력했으며, 다시 돌아갔다온다해도 그 정보는 전역으로 이미 저장이 되어있으며 만약 UI의 변경사항이 state값으로 연동이 되지 않았다면, 정보가 되게 애매하게 꼬일 것이 뻔하다.

그래서 무언가 예민한 정보를 저장할 때에는 최대한 지역상태로 관리하는 것이 맞다고 본다.

아 물론 필자는 이러한 구조의 회원가입 form을 전역상태로 관리해버렸기에 위와 같은 불상사를 막기위해 최대한으로 찾아본 결과 이렇게 처리하긴했다.

  const [page, setPage] = useRecoilState(pageNumberAtom);
  const resetAdCategory = useResetRecoilState(adCategoryAtom);
  const resetAdContents = useResetRecoilState(adContentsAtom);
  const resetAdCost = useResetRecoilState(adCostAtom);
  const resetRecommendCost = useResetRecoilState(recommendCostAtom);

  const pageDownHandler = () => {
    if (page == 3) {
      resetAdCategory();
    }

    if (page == 4) {
      resetAdContents();
      resetAdCost();
      resetRecommendCost();
    }
    setPage(page - 1);
  };

전역상태관리 라이브러리를 recoil로 선택을 했어가지고, 뒤로가기 버튼을 눌렀을 때, 이전 페이지에서 관리하는 state 값을 다시 default로 처리할 수 있게 useResetRocoilState 라는 recoil hook을 활용하여 처리하였다.

결론은

리액트 상태관리 이거이거 손가락은 어떻게 작성해야될지 습관처럼 이해했지만, 실무에서 무언가 기능을 만들게 될 때, 무작정 움직이는 손가락을 뇌가 잠시 억제하는 것 같다. 정말 이렇게 state를 처리하면 추후에 문제가 없을까? 혹은 이것이 정녕 맞는 방법일까..? 답은 개발자 마음이기도 하지만 서비스의 기획, 그리고 이 기능이 어떻게 작용되고 문제가 발생하는 경우는 어떤 것인지 잘 파악하면서 작성해야된다는 것을 현업에서 느끼게 되었다. 심지어 혼자서 계속 부딪히다 보니까 절실히 느끼는 바이기도 하다.

앞으로 다른 기능들을 개발해보면서 무언가 확신이 들 수 있는 경험치들을 쌓아가면 추후에 더 좋은 상태관리를 할 수 있지 않을까 생각하면서 이번 포스트를 마치려고한다잇

reference
react state management guide

profile
태어난 김에 많은 경험을 하려고 아등바등 애쓰는 프론트엔드 개발자

0개의 댓글