react with recoil

Overview

저번 글에서는 Recoil 의 전반적인 개념에 대해서 살펴봤습니다.

저번주 Tech Day 에서 발표를 하고난 후 Recoil 에서 Storage 를 연동해서 사용하는 방법이 궁금하다는 요청을 받고 알아보게 되었습니다.

저희 Playce Dev 팀의 FE 파트에서는 현재 Playe RORO v3 을 개발하는데 있어서 Redux 를 사용하고 있고, Redux-persist 라는 라이브러리를 활용하여 현재의 state 를 LocalStorage 혹은 SessionStorage 에 저장하여 브라우저가 종료 되었다가 다시 웹 페이지가 실행 되어도 해당 state 를 유지시키는 방법을 채택하고 있습니다.

Recoil 에서 Redux-persist 같이 자동으로 storage 와 연동하는 기능이 있을까 하는 궁금증으로 공식문서와 구글링을 시작하였고, 이번에 공부했던 내용을 예제와 함께 여러분들에게 공유해보도록 하겠습니다.

 

 

사고의 흐름

#1. atom 을 storage 에 자동으로 연동시키는 방법이 있을까?

첫 번째 궁금증은 “atom 을 storage 에 자동으로 연동하는 방법이 있을까?” 부터 시작했습니다.

사실 redux-persist 는 설정만 한다면 자동적으로 state 를 storage 에 저장해서 정말 편리하게 개발할 수 있습니다. 하지만 또 다른 의존성을 추가(라이브러리를 추가)해야 함으로써 그에 따른 댓가도 치뤄야 하는 편입니다.

본론으로 돌아가 atom 을 storage 에 자동으로 연동하는 방법은 atom 의 effect 를 활용하는 것입니다!

atom 의 effect 란 React 의 useEffect 와 비슷합니다. atom 은 어플리케이션이 실행될 때 App 컨텍스트의 외부에서 생성됩니다. atom 의 effect 는 atom 이 생성될 때 실행되는 부수 효과 (Side Effect) 입니다.

export const user = atom<IUsertemp>({
  key: "user",
  default: {} as IUsertemp,
  effects: [localStorageEffect("user"), sessionStorageEffect("user")],
});

먼저 atom 을 생성해 줍니다. 그리고 인자로 넘겨주는 프로퍼티에 effects 라는 프로퍼티를 추가해주고, 의존성으로 effect 함수들을 넣어줍니다.

const localStorageEffect =
  (key: string) =>
  ({ setSelf, onSet }: any) => {
    const savedValue = localStorage.getItem(key);

    if (savedValue !== null) {
      setSelf(JSON.parse(savedValue));
    }

    onSet((newValue: any, _: any, isReset: boolean) => {
      isReset
        ? localStorage.removeItem(key)
        : localStorage.setItem(key, JSON.stringify(newValue));
    });
  };

const sessionStorageEffect =
  (key: string) =>
  ({ setSelf, onSet }: any) => {
    const savedValue = sessionStorage.getItem(key);

    if (savedValue !== null) {
      setSelf(JSON.parse(savedValue));
    }
    onSet((newValue: any, _: any, isReset: any) => {
      const confirm = newValue.length === 0;
      confirm
        ? sessionStorage.removeItem(key)
        : sessionStorage.setItem(key, JSON.stringify(newValue));
    });
  };

LocalStorage, SessionStorage 에 저장하는 effect 함수입니다. 매개변수의 key 는 저장소에 저장되는 key 값이며, 내부의 콜백 함수에서의 객체 속 매개변수로 주어지는 setSelf 함수는 연결된 atom 의 값을 초기화 해주는 함수이며 onSet 함수는 해당하는 atom 의 값이 변경이 되었을 때 실행되는 함수입니다.

 

#2. atom 값을 서버 데이터로 초기화 할 순 없을까?

두 번째 궁금증은 atom 값을 서버 데이터로 초기화 하는 방법이였습니다.

어플리케이션이 실행 되었을 때, 초기 데이터가 필요하여 서버에서 데이터를 가져와 전역 상태로 초기화 시켜야 하는 경우가 있습니다. 이런 경우 atom 에 데이터를 어떻게 초기화 할 수 있을까요?

먼저 async Effect 함수를 정의하여 effects 의 의존성 배열에 넣어줍니다.

const asyncUserListEffect =
  (key: string) =>
  ({ onSet, setSelf }: any) => {
    setSelf(() => {
      const localData = localStorage.getItem(key);

      //localStorage 에 셋팅된 값이 있다면 해당 값으로 atom 을 초기화, 없다면 API 호출
      if (localData !== null) {
        return JSON.parse(localData);
      } else {
        return getUserList();
      }
    });

    // Trigger 가 발동이 되어야 실행된다. (atom 의 값이 변경이 되었을 경우에 실행된다.)
    onSet((newValue: any, _: any, isReset: boolean) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    });
  };

// Effects 를 활용해 atom 의 초기값 설정
export const userList = atom<IUser[]>({
  key: "userList",
  effects: [asyncUserListEffect("list")],
});

위의 로직을 살펴보면, 먼저 로컬 저장소에 저장된 데이터가 있는지 체크하고, 만약 데이터가 없다면 데이터를 패칭하여 atom 값을 초기화 시켜줍니다.

선언된 atom 을 살펴보면 default 프로퍼티를 설정하지 않았습니다. atom 이 생성될 때 effects 배열에 저장된 함수들이 실행되면서 atom 의 값을 초기화 시키기 때문에 따로 설정하지 않았습니다.

 

#3. 만약 동적인 서버 데이터를 atom 에 초기화 할 수 있는 방법은?

웹 어플리케이션을 개발하다보면 반드시 동적인 데이터를 활용해야 하는 경우가 있습니다. 그럴 경우에 어떻게 해야할까요?

atom 대신 atomFamily 를 활용해 매개변수를 활용 해봅니다.

import { atom, atomFamily, selector } from "recoil";
import { IUser } from "../type";
import { getSelectedUser } from "./api";

const asyncUserEffect =
  (key: string, id: number) =>
  ({ onSet, setSelf }: any) => {
    setSelf(() => {
      const localData = localStorage.getItem(key);

      //localStorage 에 셋팅된 값이 있다면 해당 값으로 atom 을 초기화, 없다면 API 호출
      if (localData !== null) {
        return JSON.parse(localData);
      } else {
        return getSelectedUser(id);
      }
    });

    // Trigger 가 발동이 되어야 실행된다. (atom 의 값이 변경이 되었을 경우에 초기화된다.)
    onSet((newValue: any, _: any, isReset: boolean) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    });
  };

// 첫 번째 제네릭은 아톰의 타입, 두 번째 제네릭은 param 의 타입
export const userAtom = atomFamily<IUser, number>({
  key: "userAtom",
  effects: (param) => [asyncUserEffect("userAtom", param)],
});

atomFamily 를 활용해 매개변수를 받습니다. effects 프로퍼티의 값으로 함수를 넣어주는데, 매개변수 param 을 받아서 해당 param 을 effect 함수에 넣어서 사용할 수 있습니다.

atomFamily 의 제네릭은 두 가지를 넣어줘야 합니다. 첫 번째 제네릭은 해당 atom 의 값의 타입이며, 두 번째 제네릭은 parameter 의 타입입니다.

import React from "react";
import { useRecoilState } from "recoil";
import { IUser } from "../modules/type";
import { userAtom } from "../modules/User/atom";

// UserList 에서 하나의 User 만 저장하는 상황
// atom 에 파라미터가 필요하다.
// localStorage 와 연동

type FamilyProps = {
  id: number;
};

export default function Family(props: FamilyProps) {
  const { id } = props;
  const [oneUser, setOneUser] = useRecoilState<IUser>(userAtom(id));
  return (
    <div>
      <h1>{oneUser.id}</h1>
      <h3>{oneUser.name}</h3>
    </div>
  );
}

간단한 예제 컴포넌트 입니다. props 로 주어지는 값에 따라서 atom 값을 다르게 초기화 해봅니다.

 

#4. Cookie 를 활용하고 싶다?

마지막으로 Cookie 에 값을 저장해보도록 하겠습니다.

아쉽게도, Recoil 에서는 Cookie 와 연동되는 기능이 없습니다. 그렇기 때문에 쉽게 Cookie 를 활용하기 위해서 react-cookie 라이브러리를 활용해 봤습니다.

import React, { useState } from "react";
import { useCookies } from "react-cookie";
import { useSetRecoilState } from "recoil";
import { user } from "../modules/UserList/atom";
import { IUsertemp } from "../modules/type";

function Join() {
  const [id, setId] = useState("");
  const [pwd, setPwd] = useState("");
  const [name, setName] = useState("");
  const [addr, setAddr] = useState("");
  const [coockies, setCoockie, removeCookie] = useCookies(["user"]);

  const handleSaveCookie = () => {
    const newbby: IUsertemp = {
      id,
      pwd,
      name,
      addr,
    };
    setCoockie("user", newbby);
  };

  return (
    <>
      <form onSubmit={handleSubmit}>
        <div>
          <span>ID</span>
          <input value={id} onChange={handleId} />
        </div>
        <div>
          <span>PWD</span>
          <input value={pwd} onChange={handlePwd} />
        </div>
        <div>
          <span>NAME</span>
          <input value={name} onChange={handleName} />
        </div>
        <div>
          <span>ADDR</span>
          <input value={addr} onChange={handleAddr} />
        </div>
        <button onClick={handleSaveCookie}>save in Cookie</button>
      </form>
      {/* <div>{coockies.user.id}</div> */}
    </>
  );
}

export default Join;

간단한 예제입니다. change 함수는 예제가 복잡해지니 예제에서 제외했습니다.

버튼을 클릭 시 “user” 라는 key 를 가진 쿠키에 데이터가 저장이 됩니다. useCookies 훅을 활용하면 쉽게 리엑트에서 쿠키에 값을 저장할 수 있습니다.

 

#5. 모듈의 구조

type, api, atom 을 모듈화 시킨 구조를 살펴봅시다.

Recoil 을 활용하여 모듈화 시킨다면 위와 같은 구조로 모듈화 시킬수 있지 않을까 생각합니다.

 

 

recoil-persist

Redux 를 사용할 때 상태를 유지시키기 위해 redux-persist 라이브러리를 사용합니다. 그렇다면 Recoil 에서 동일한 기능을 제공하는 라이브러리는 없을까요?

recoil-persist 라는 라이브러리가 존재하긴 합니다.

npm install recoil-persist
yarn install recoil-persist

먼저 라이브러리를 다운 받은 후, recoilPersist 를 활용해 persistAtom 을 return 받습니다. 아래 코드에서는 Javascript Destructured 구조를 활용해 간단하게 persistAtom 만 리턴받았습니다.

// sessionStorage 와 localStorage 에 동시에 적용할 순 없음
const { persistAtom } = recoilPersist({
  key: "recoil-persist-atom",
  storage: sessionStorage,
  // storage: localStorage,
});

// atom 집합은 React 컨텍스트 외부에서 생성된다
// atom 의 초기값을 서버 데이터로 초기화 하고 싶다면 effects 를 활용!!
export const user = atom<IUsertemp>({
  key: "user",
  default: {} as IUsertemp,
  // effects: [localStorageEffect("user"), sessionStorageEffect("user")],
  effects: [persistAtom],
});

persistAtom 을 effects 배열에 넣어주기만 하면 됩니다. 간단한 코드로 자동으로 상태를 유지시킬 수 있습니다.

하지만 recoil-persist 를 도입하기에 앞서 여러가지 사항을 고려해야 합니다.

  1. Recoil 버전
    현재 22년 5월 20일 기준 최신 Recoil 버전은 0.7.1-alpha-2 입니다. 하지만 recoil-persist 는 Recoil 0.6.1 버전을 지원하고 있습니다.
  2. 안정적이지 않은..
    최근 업데이트가 1년이나 더 되었을 정도로 관리가 되고 있는 라이브러리가 맞나 의문이 듭니다.

Recoil 의 업데이트 주기는 생각보다 짧습니다. 그 만큼 버전이 호환이 안되는 문제가 발생할 수 있습니다.

 

 

예제 Repository

깃허브 : 바로가기

 

 

마무리하며..

Recoil 은 Redux 에 비해서 레퍼런스가 적습니다. 그래서 effect 에 관련된 내용을 구글링 하여도 원하는 정보가 나오지 않았습니다. 해당 포스팅은 공식 문서를 살펴보면서 직접 테스트 해보며 작성한 내용입니다.

이번 조사를 마친 뒤 Recoil 이 보다 정교하다는 생각이 들었고, 충분히 새로운 아키텍처에 적용하여 활용해 볼 수 있겠다 라는 생각이 들었습니다. 또한 Recoil 팀이 계속해서 업데이트를 해주기 때문에, 앞으로의 기능을 기대해봐도 좋을것 같다는 생각을 해봅니다.

읽어봐주셔서 감사합니다.

강동희
Developer

오픈소스컨설팅 Playce Dev Team 소속으로 오늘을 바탕으로 내일이 더 기대되는 프론트엔드 개발자 강동희입니다. 빠르게 변화하는 기술들에 관심이 많으며 지식 공유에 큰 가치를 두고 있습니다.

Leave a Reply

Your email address will not be published. Required fields are marked *

One reply on “Recoil with Storage (feat. effects)”