상태 관리를 위한 Recoil 의 적용

Moon Works·2022년 12월 22일
0
post-thumbnail

상태관리의 필요성

next.js 로 사이드 프로젝트 시작!에서 언급한 것 처럼 가변고 간단한 음력 달력 서비스를 만들어보았다.
혹시나 누군가에게 조금이나마 바라며 링크를 덧붙인다.

만들고 나니 특정날짜를 선택하고 싶어졌다.

다음과 같은 마크업 구조를 기반으로 처음 고려했던 방식은 특정 날짜를 선택하면 이벤트 핸들러로 날짜를 전달 받고 "선택된 날짜"를 각 날짜 <cell> 로 다시 전달한다. 각 cell 은 "선택된 날짜"와 자신의 날짜가 동일하면 선택된 상태를 표기하는 방식이다.

<App>
  <Calendar>
    <MonthView>
      <!-- row 는 한 (week) 에 해당하는 요소 -->
      <row>
        <!-- cell 은 하나의 날짜(date)를 의미 -->
        <cell /><cell /><cell /><cell /><cell /><cell /><cell />
      </row>
      <row></row>
      <row></row>
      <...>
    </MonthView>
  </Calendar>
</App>

그런데 체감적으로 느렸다. 개발자 도구에서 측정한 결과 역시 느렸다.

Chrome 의 플러그인 React 프로파일러는 측정한 결과 날짜를 선택할 때마다 3개의 캘린더 요소의 각 하위 요소인 MonthView 에서 그 하위의 각 cell 로 전파되는 렌더링 비용을 보여주었다.

Chrome DevTools 의 Perfomance 탭에서 측정한 결과 JS 비용만 110ms 를 초과했다. 부드러운 움직임(60fps)을 위해 16ms 내에 출력해야한다는 측면에서 매우 별로인 결과이다.

각 cell 이 변경되지 않았더라도 선택된 날짜 정보를 (Celendar 를 감싸고 있는)부모가 전달 받아 state 를 변경했으니 그에 포함된 모든 컴포넌트 렌더링을 유발하는 것은 당연한 결과이다.

선택된 날짜 정보를 cell 에서 상위 컴포넌트로 전달할 필요 없이 각 <cell> 만 리렌더링하는 것이 효율적이라 생각했다.

처음에는 useContext() 만을 이용해서 상태를 관리하고자 했다. useContext 를 이용하면 여러 Depth 로 중첩된 하위 컴퍼넌트로 파라미터를 줄줄이 전달해 주지 않는다는 이점이 있다.
참고 - [React] 리액트 Hooks : useContext() 함수 사용법 (전역 상태 관리)
그러나 상태 변경 시 렌더링 할 필요없는 컴포넌트까지 다시 리렌더링을 유발하므로 내가 갖는 문제를 해결해 주지 않는다.

결론적으로 상태관리 라이브러리 도입 필요성을 느꼈다.

즉 다른 날짜로 선택했을 때 각 cell은 굳이 상위로 "선택된 날짜"를 전달하고 상태를 변경하여 전체를 리렌더링하는 것이 아니라 cell 자체적으로 공유된 상태값을 변경하고 변경을 감지한 각 <cell> 에서만 리렌더링 하는 것이다.

결론적으로 큰 성능의 개선이 있었다.

Chrome Dev Tools 에서는 Recoil 만 사용해서도 꽤 높은 성능 개선(24ms)이 있었지만 추가적으로 useMemo 를 적용하여 11ms 수준으로 올릴 수 있었다.

Recoil 을 선택한 이유

그럼 왜 Recoil 을 선택했나?

  1. 기존에 사용했던 mobx 외에도 다른 것을 써보고 싶었다.
  2. 많이 사용하는 Redux 의 복잡한 기능까지는 필요없었다.
  3. 다음 이유로 React 에 더 최적화 되었을 것 같은 느낌이 들었다.
  • React 만 지원
  • facebook 에서 만듬
  1. Recoil 이 요구사항에 맞는 심플한 기능을 제공해주었고 사용방식이 단순해 보였다. (역시 쉬워야 돼...)

Recoil 의 주요 요소

<RecoilRoot>

변경을 감지할 대상을 감싸야 한다. 보통은 최상위에 감싼다. 나는 대략 다음과 같이 감쌌다. 각 Calendar 콤포넌트가 포함한 "날짜<cell>" 에서 상태 변경을 감지하거나 상태를 변경하기 위함이다.

<App>
  <RecoilRoot>
    <Flicking>
      <!-- 3개의 달력 패널 -->
      <Calendar />
      <Calendar />
      <Calendar />
    </Flicking>
  </RecoilRoot>
</App>

atom()

핵심 요소이다.

  • 콤포넌트 간에 공유할 데이터를 담는 그릇으로 생각하면 된다
    • recoil 개발자는 atom 을 데이터를 담는 비눗방울로 표현하더라.. 둥둥 떠서 공유되어서 그렇다나..
  • 쓰기 가능한(writable) 상태 값을 반환한다.

아래 그림처럼 atom(노란색 비눗방울)은 둥둥 떠서 비눗방울 값을 참조하고자 하는 React Component 에게 값을 제공하거나 변경할 수 있게 된다.

그냥 아래와 같이 선언만 해두면 전역에서 'key' 로 관리되어진다.

const selectedDateState = atom({
  key: 'selectedDateState',
  default: null,
});

export selectedDateState;

우리는 atom 의 반환 값을 useRecoilState()useRecoilValue() 를 통해 원하는 값을 얻거나 변경할 수 있게 된다.

나 같은 경우는 State 를 별도 파일에 모아두어 필요한 곳에서 import 하여 사용했다.

import { selectedDateState } from './CalendarState';

selector()

  • atom 에 저장된 값을 가공하여 반환하거나 필터링하고 싶을 때 사용한다.

직접 사용하지는 않아 Recoil 공식 페이지의 예제를 빌리면 다음과 같다.

const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => {
    const fontSize = get(fontSizeState);
    const unit = 'px';

    return `${fontSize}${unit}`;
  },
});

fontSizeState 는 atom 이다. 이 fontSizeState 에 "px" 을 붙여서 반환 받고 싶은 경우 selector 를 선언하고 get 메소드를 구현하면 된다.

  • 기본적으로 selector 는 읽기만 가능하지만 필요에 따라 setter 를 지정할 수도 있다. (이와 관련된 내용은 나중에 다룰 기회가 있을 것 같기도 하다.)

useRecoilState ? useRecoilValue ?

  • useRecoilState
    • atom 을 인자로 값과 그 값에 대한 Setter 를 반환한다.

atom 으로 만든 selectedDateState 를 import 하여 useRecoilState 를 사용한 예제이다.

import { selectedDateState } from './CalendarState';

const MyDateCellWrapper = (props) => {
  const { children, value } = props;
  const [selectedDate, setSelectedDate] = useRecoilState(selectedDateState);
  /**
   * 선택된 날짜를 얻어온다.
   */
  const isSelected = moment(value).isSame(selectedDate, "day");

  // 선택상태가 변경되었을 때에만 다시 렌더링 한다.
  const dateCellWrapper = useMemo(() => {
    return (
      <div
        className={`${styles.wrap} ${isSelected ? styles.selected : ""} ${
          isSelected ? "selected" : ""
        }`}
        onClick={() => {
          /**
           * 선택된 날짜를 변경한다.
           */
          setSelectedDate(value);
        }}
      >
        {children}
      </div>
    );
  }, [isSelected]);

값(selectedDate) 와 값에 대한 setter(setSelectedDate) 를 얻어와 활용할 수 있다.

const [selectedDate, setSelectedDate] = useRecoilState(selectedDateState);
  • useRecoilValue
    • useRecoilState 가 setter 를 반환하는 반면 useRecoilValue 는 값만을 반환한다.
    • 상태 변경 없이 상태 값만 참조하는 경우에 사용한다.
const selectedDate = useRecoilValue(selectedDateState);

사용사례

내 달력 사이드프로젝트 에서는 날짜 선택외에도 손 없는 날을 표기할지 여부도 Recoil 을 활용했다.

완전히 Calendar 와 독립적인 메뉴 영역에서 isShowNoGhostDayState 상태를 변경하면 각 <cell> 에서는 변경사항을 감지하여 컴퍼넌트를 다시 렌더링한다.

/**
 * 손없는 날
 */
export const isShowNoGhostDayState = atom({
  key: "isShowNoGhostDay",
  default: true,
});

소감

쉽게 느껴지는 점이 강점이다. 기본적인 기능 사용을 위해 알아야 하는 개념이 많지가 않다. 1) 먼저 상태 감지가 필요한 영역을 <RecoilRoot> 로 감싸고 2) atom 으로 값을 초기화한 후 3) useRecoilValue 혹은 useRecoilState 로 값을 사용하는 것 만으로도 충분하다. 크게 개념이 다르지 않지만 mobx 대비 좀 더 React 스럽게 사용하는 느낌이다.

이 외에도 Recoil 은 많은 기능을 다루고 있다. 이런 기능들을 사용해볼 기회가 있으면 좋겠다는 생각이 든다.

참고자료

profile
계속 성장하고 싶은 개발자. 사이드프로젝트(https://www.month2k.com)

0개의 댓글