Recoil로 상태관리하기(feat. SSR...)

이예슬·2022년 11월 13일
0

React에서 상태 관리가 중요한 이유!

React에서 데이터의 흐름

단방향 바인딩을 하는 라이브러리인 react는 부모 컴포넌트에서 자식컴포넌트로만 state를 props로 전달할 수 있고 자식의 props를 부모에게 전달하는 방법은 존재하지 않는다.

Flux 패턴

기존의 MVC 패턴의 단점을 해결하기 위해 Flux 패턴이 등장했다.

Action은 데이터의 상태를 변경하는 명령을 의미하고 Dispatcher는 Action을 감지하여 Store에 전달하는 역할을 한다. Model은 Store라고 볼 수 있으며 state가 저장되어 있는 공간을 의미한다. 마지막으로 View는 해당 데이터들을 가지고 와서 화면에 렌더링하는 것을 의미한다.

대신 자식 컴포넌트에서 부모 컴포넌트의 state를 바꿀 수 있는 두 가지 방법이 존재한다.

  1. setState 를 props로 넘겨준다.

  2. 상태 관리 툴을 사용한다.(redux, recoil… )

  3. Context API를 사용한다.

    react에 내장된 context API는 theme와 같은 낮은 빈도의 업데이트에는 좋은 선택일 수 있지만 Context는 Flux와 같은 상태 관리 시스템을 대체할 수 없다.

    Context API는 Provider의 속성이 변경될 때마다 Provider 하위의 모든 Consumer들을 렌더링한다.

Recoil : A state management library for React

  • 작고 React스러운
  • 데이터 흐름 파생 데이터와 비동기 쿼리는 순수 함수와 효율적인 구독으로 관리된다.
  • 교차하는 앱 관찰 코드 분할을 손상시키지 않고 앱 전체의 모든 상태 변경을 관찰하여 지속성, 라우팅, 시간 이동 디버깅 또는 실행 취소를 구현한다.

Atom

An atom represents state in Recoil. The atom function returns writeable RecoilState Object

atom은 고유한 key를 갖는 컴포넌트가 구독할 수 있는 가장 작은 단위의 상태값이 되도록 구성하는 것이 이상적이다.

아래 그림에서 알 수 있듯이 atom이 업데이트되면 해당 atom을 구독하고 있던 모든 컴포넌트들의 state가 새로운 값으로 리렌더링 되기 때문에 atom을 잘 설계해야 렌더링에서의 이점을 얻을 수 있다.

userAtom.js

export const isLoginState = atom<boolean>({
  key: 'isLogin', //unique Id
  default: false, // default value
 });

recoil 사용 예

const [isLogin, setIsLogin] = useRecoilState(isLoginState);
const isLogin = useRecoilValue(isLoginState); // value만
const setIsLogin = useSetRecoilState(isLoginState); //set만

const resetIsLogin = useResetRecoilState(isLoginState) // atom값을 reset한다. 
resetIsLogin()

AtomFamily

// 첫번째 제네릭은 atom의 type, 두번째 제네릭은 param의 타입 
export const userAtom = atomFamily<User, number>({
  key: "userAtom",
  default: (name, age) => ({
    name,
	  age
  }),
});

atomFamily는 동일한 형태의 atom을 생성해주는 함수를 제공하며 atomFamily를 호출할 때마다 지정한 형식의 atom을 생성해낸다.

atom과의 차이점은 default 값이 특정한 파라미터를 받는 함수가 될 수 있다는 점이다.

Selector

atom만으로는 비동기 처리를 하기 어렵다. selector는 recoil에서 비동기 처리를 할 수 있도록 도와준다.

A selector represents a piece of derived state

Selector는 파생된 상태(derived state)의 일부를 나타낸다.

⇒ atom을 원하는 대로 변형해 return 받을 수 있다. (selector는 readonly한 값만을 반환)

Selector get 예제

// atom.ts

import { atom, selector } from "recoil";

export type status = "DONE" | "DOING";

interface toDo {
  status: status;
  contents: string;
}

export const selectStatus = atom<status>({
  key: "nowStatus",
  default: "DOING"
});

export const toDos = atom<toDo[]>({
  key: "toDos",
  default: [
    { status: "DOING", contents: "default 1" },
    { status: "DONE", contents: "default 2" },
    { status: "DONE", contents: "default 3" },
    { status: "DOING", contents: "default 4" },
    { status: "DOING", contents: "default 5" }
  ]
});

export const selectToDo = selector<toDo[]>({
  key: "selectToDos",
  get: ({ get }) => {
    const originalToDos = get(toDos);
    const nowStatus = get(selectStatus);
    return originalToDos.filter((toDo) => toDo.status === nowStatus);
  }

});

Selector set 예제

selector는 set을 활용해 atom의 값을 변형해줄 수 있으며 이 과정에서 비동기 통신 처리를 할 수도 있다.

export const selectToDo = selector<toDo[]>({
  key: "selectToDos",
  get: ({ get }) => {
    const originalToDos = get(toDos);
    const nowStatus = get(selectStatus);
    return originalToDos.filter((toDo) => toDo.status === nowStatus);
  },
  set: ({ set }, newToDo) => {
    set(toDos, newToDo); // (변경할 atom, 변경해줄 값)
  }
}
);

Selector를 활용한 비동기 통신

export const selectId = atom({
  key: "selectId",
  default: 1
});

export const selectingUser = selector({
  key: "selectingUser",
  get: async ({ get }) => {
    const id = get(selectId);
    const user = await fetch(
      `https://localhost:3000/users/${id}`
    ).then((res) => res.json());
    return user;
  },
  set: ({ set }, newValue) => {
    set(nowUser , newValue);
  }

});

parameter를 동적으로 넘겨주고 싶을 때

export const selectUser = selectorFamily({
  key: "selectOne",
  get: (id: number) => async () => {
    const user = fetch(
      `https://localhost:3000/users/${id}`
    ).then((res) => res.json());
    return user;
  }
});

// 컴포넌트에서 사용 시 

const user = useRecoilValue<User>(selectUser(id));

💡 Selector를 활용해 비동기 통신을 하면 캐싱된 값을 사용할 수 있다!

Recoil with SSR

서버 사이드 렌더링은 react 컴포넌트를 클라이언트가 아닌 서버에서 렌더링해서 결과를 반환한다.

이 결과물은 HTML 파일 형식으로 전송되며 안에 든 내용은 정적인 HTML 문자열이다.

전송받은 결과물은 정적인 HTML 파일이므로 이를 React node와 일치시키는 작업이 필요한데 이 과정을 hydrate 라고 한다.

로그인 기능을 개발하면서 새로고침 시 나타났던 hydration error는 바로 이 hydrate과정에서 initial UI와 전달받은 HTML 파일이 일치하지 않아 발생한 에러였다.

상태와 컴포넌트의 렌더링 상태가 항상 동기화 되어 있어야 하므로 recoil과 같은 상태 관리 라이브러리를 사용한다면 상태의 스냅샷 또한 함께 전달되어야 한다. 전달된 상태의 스냅샷은 상태 보관 스토어에 넣으면 된다.

recoil은 <RecoilRoot>를 호출하면 내부에서 자동으로 스토어가 만들어지며 직접 스토어를 다룰 수 없다. 그러므로 <RecoilRoot>initialState 라는 prop을 통해 초기화 함수를 전달 받을 수 있다.

export default function MyApp({ Component, pageProps }: AppProps) {
  
	const initializeRecoilState = ({ set }: any) => {
    set(isLoginState, true);
  };

  return (
    <RecoilRoot initializeState={initializeRecoilState}>
          <Layout>
            <Component {...pageProps} />
          </Layout>
    </RecoilRoot>
  );
}

initializeRecoilState를 사용할 때 initial 값을 어떻게 넣어줘야 할지에 대해서는 공부가 좀 더 필요할 것 같다.

profile
꾸준히 열심히!

0개의 댓글