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 를 도입하기에 앞서 여러가지 사항을 고려해야 합니다.
- Recoil 버전
현재 22년 5월 20일 기준 최신 Recoil 버전은 0.7.1-alpha-2 입니다. 하지만 recoil-persist 는 Recoil 0.6.1 버전을 지원하고 있습니다. - 안정적이지 않은..
최근 업데이트가 1년이나 더 되었을 정도로 관리가 되고 있는 라이브러리가 맞나 의문이 듭니다.
Recoil 의 업데이트 주기는 생각보다 짧습니다. 그 만큼 버전이 호환이 안되는 문제가 발생할 수 있습니다.
예제 Repository
깃허브 : 바로가기
마무리하며..
Recoil 은 Redux 에 비해서 레퍼런스가 적습니다. 그래서 effect 에 관련된 내용을 구글링 하여도 원하는 정보가 나오지 않았습니다. 해당 포스팅은 공식 문서를 살펴보면서 직접 테스트 해보며 작성한 내용입니다.
이번 조사를 마친 뒤 Recoil 이 보다 정교하다는 생각이 들었고, 충분히 새로운 아키텍처에 적용하여 활용해 볼 수 있겠다 라는 생각이 들었습니다. 또한 Recoil 팀이 계속해서 업데이트를 해주기 때문에, 앞으로의 기능을 기대해봐도 좋을것 같다는 생각을 해봅니다.
읽어봐주셔서 감사합니다.