본문 바로가기

JavaScript/React

[React] Jotai로 상태 전역으로 관리하기

728x90
반응형

간단한 사이드 프로젝트를 진행하면서 기존에 사용하던 Zustand 말고 새로운 상태 관리 도구를 사용하고 싶은 생각이 들게 되어 Redux, Recoil, Jotai 중에서 고민을 하다 Jotai를 채택하고 진행하기로 하였다.

 

1. Jotai를 선택한 이유

이유를 설명하기 전 Jotai 특징과 장단점에 대해 간략하게 알아보자.

 

  • Jotai는 일본어로 '상태'라는 뜻이다. Recoil에 영감을 받아 만들어졌다고 하며, Recoil과 유사하게 아토믹(Atomic) 모델과 함께 bottom-up 방식으로 접근한다. 이러한 방식은 작은 아톰들을 차곡차곡 쌓아 큰 형태로 만들어 나가는 느낌으로 이러한 bottom-up 방식은 성능이 중요한 앱에서 많이 사용이 된다.
  • Jotai는 매우 작은 번들 크기로 돌아가기에 부담 없이 가볍게 사용하기 좋은 도구이다. 작은 규모의 프로젝트나 간단한 상태관리를 위해 사용하면 매우 좋다.
  • 배우기 또한 매우 쉽다. React의 useState와 유사한 모양이기 때문에 쉽게 입문할 수 있고 TypeScript를 기반으로 하며, NextJS 및 React-native에도 지원이 되기에 현재 React를 사용하고 있고 계속 React의 길을 갈 것이라면 사용 시 이점이 많다. 다만 단점으로 아직까진 관련 자료가 적어 참고할 레퍼런스가 부족하다.

 

이러한 Jotai의 특징들을 보면 현재 내가 진행하려던 사이드 프로젝트에 매우 적합하였다. 작은 규모로 진행할 것이기에 Redux를 적용하기엔 과하다는 생각이 들었고 비교적 경량화된 Recoil과 Jotai 중 고민을 하다 Jotai가 좀 더 괜찮은 것 같아서 선택하게 되었다.

 

 

 

 

 

 

2. Recoil?

위에서 서술했듯이 Jotai는 Recoil에서 영감을 얻어 만들어지게 되었다. Jotai 자료를 찾다 보면 심심치 않게 접할 수 있는데 Recoil은 2020년 Facebook(현 meta)에서 발표한 React의 상태 관리 라이브러리로 Jotai의 원조격답게 경량화되어 있으며, 아토믹(Atomic) 모델과 bottom-up 방식으로 이루어져 있다.

사용법도 간단하고 무엇보다 Jotai와 달리 관련 커뮤니티가 활성화되어있어 참고할 자료와 레퍼런스 많다는 장점이 있다. 그래서 Jotai보다 편하게 사용할 수 있을지도 모르겠으나 아쉽게도 Recoil에는 여러 이슈가 존재하였다.

 

SSR 지원이 미비하거나 메모리 누수 등의 이슈가 있지만 무엇보다 Recoil의 미래는 어두울 것이라는 점이다.

 

https://github.com/facebookexperimental/Recoil/tags

 

위 그래프를 보면 Recoil은 시간이 지날수록 유사한 구조인 Jotai에게 밀리고 있다.

현재 Recoil은 업데이트가 이루어지지 않고 있으며, 이로 인해 다른 라이브러리에 비해 버전이 매우 낮고 이슈가 많은 상태임을 확인할 수 있다. (마지막 업데이트가 작년 3월이다..)

찾아보니 Recoil을 설계하고 개발을 진행했던 책임자의 퇴직으로 인한 영향으로 Recoil의 성장이 멈추게 된 것 같다.

 

이러한 Recoil 이슈로 인해 업데이트가 꾸준히 진행되고 SSR을 지원하는 Jotai를 선택하게 된 것이다.

728x90

 

 

 

 

 

3. Jotai 다루기

이제 Jotai를 직접 다뤄보자 

# with npm
npm i jotai

# with yarn
yarn add jotai

 

 

jotai는 useState와  유사한 모양이기도 하고 사용법 또한 비슷하기에 그냥 useState라고 생각하고 사용하면 편하다. 

import { atom, useAtom } from "jotai";
import { useEffect } from "react";

const test1 = atom<number>(0);

const JotaiTest = () => {
    const [testVal, setTestVal] = useAtom(test1);

    useEffect(() => {
        console.log("값 확인 : " + testVal);
    }, [testVal])

    return (
        <div>
            <button onClick={() => setTestVal(testVal+1)}>click</button>
        </div>
    )
}

export default JotaiTest;

 

우선 jotai를 import 해주고 atom 상태를 정의해 준다. 마치 useState 초기값을 설정해 주듯이 작성해 주면 된다.

그리고 useAtom을 통해 내가 선언한 아톰을 가져와서 사용한다. useAtom 안에는 선언한 atom의 이름을 넣어주면 된다. 간단하게 클릭 이벤트로 해당 atom 상태를 1씩 증가시켜 주어서 테스트해 보자

 

jotai는 useAtom 뿐만이 아닌 아래와 같은 형태로도 상태를 읽고 쓸 수 있다.

import { atom, useAtomValue, useSetAtom } from "jotai";
import { useEffect } from "react";

const test1 = atom<number>(0);

const JotaiTest = () => {
    const testVal = useAtomValue(test1);  // read
    const setTestVal = useSetAtom(test1); // write

    useEffect(() => {
        console.log("값 확인 : " + testVal);
    }, [testVal])

    return (
        <div>
            <button onClick={() => setTestVal(testVal + 1)}>click</button>
        </div>
    )
}

export default JotaiTest;

 

useAtomValue은 값을 읽기만 하고 useSetAtom은 값을 쓰기만 한다. 이 둘은 useAtom과 달리 단지 읽기만 하거나 쓰기만 하기에 리렌더링이 일어나지 않는다는 장점이 있다. 

 

Jotai는 action을 통해 비즈니스 로직을 정의하고 사용할 수 있다.

import { atom, useAtom } from "jotai";
import { useEffect } from "react";

const test1 = atom<number>(0);
const testAction1 = atom((get) => get(test1), (get, set) => {
    const val = get(test1);
    const res = val + 1;
    set(test1, res);
});

const JotaiTest = () => {
    const [testVal, setTestVal] = useAtom(testAction1);

    useEffect(() => {
        console.log("값 확인 : " + testVal);
    }, [testVal])

    return (
        <div>
            <button onClick={() => setTestVal()}>click</button>
        </div>
    )
}

export default JotaiTest;

 

상태값 +1을 action의 비즈니스 로직에 정의하고 선언한 action 이름을 useAtom에 넣어준다. 그리고 set을 호출하면 로직이 동작하여 상태값이 +1이 되고 get을 통해 업데이트된 상태값을 바로 확인할 수 있다.

 

반응형

 

action 또한 읽기만 하고 쓰기만 할 수 있다.

import { atom, useAtom } from "jotai";
import { useEffect } from "react";

const test1 = atom<number>(0);

const testAction1 = atom((get) => get(test1) + 2); // 2를 더하고 바로 읽어온다.
const testAction2 = atom(null, (get, set) => {	   // set을 할때마다 1을 더한다. 값을 바로 읽진 못한다.
    const val = get(test1);
    const res = val + 1;
    set(test1, res);
});

const JotaiTest = () => {
    const [testVal] = useAtom(testAction1); 	 // read
    const [, setTestVal] = useAtom(testAction2); // write

    useEffect(() => {
        console.log("값 확인 : " + testVal);
    }, [testVal])

    return (
        <div>
            <button onClick={() => setTestVal()}>click</button>
        </div>
    )
}

export default JotaiTest;

 

set을 할 때마다 2를 더해주진 않는다. 처음 초기값에 2를 더해주고 불러온 뒤 set에 의해서만 값이 증가한다.

get action과 set action을 합쳐주면 아래와 같이 작성할 수 있다.

import { atom, useAtom } from "jotai";
import { useEffect } from "react";

const test1 = atom<number>(0);

const testAction1 = atom((get) => get(test1) + 2, (get, set) => { // null 자리에 get을 넣어준다.
    const val = get(test1);
    const res = val + 1;
    set(test1, res);
});

const JotaiTest = () => {
    const [testVal, setTestVal] = useAtom(testAction1); // read & write

    useEffect(() => {
        console.log("값 확인 : " + testVal);
    }, [testVal])

    return (
        <div>
            <button onClick={() => setTestVal()}>click</button>
        </div>
    )
}

export default JotaiTest;

 

컴포넌트 하나하나 안에 선언하고 사용해도 되긴 하지만 다른 컴포넌트로 전달하는 작업이 조금 번거롭다. export를 사용하면 되겠지만 하나의 컴포넌트에서만 다룰 거면 그냥 useState를 쓰고 말지 굳이 Jotai로 저렇게 사용하기에는 별로다.

 

보다 Jotai를 쉽고 간편하게 전역으로 사용하기 위해 따로 atom 컴포넌트와 action 컴포넌트를 만들고 사용해 주자

그리고 위와 같은 간단한 동작 아닌 객체와 배열을 사용하여 좀 더 고급지게 구성해 보자

 

 

 

 

 

 

 

 

 

 

4. Jotai 더 잘 다루기

동적으로 Input을 추가 및 삭제하는 기능과 입력 값 수정 기능을 Jotai를 통해 구현을 해보겠다.

우선 atom과 action을 전역으로 편하게 사용하기 위해 각각 컴포넌트를 생성해 준다.

// testAtoms.tsx
import { atom } from "jotai";

export const dataArr = atom<{id:number, content:string}[]>([{id:0, content:''}]);
// testActions.tsx
import { atom } from "jotai";
import { dataArr } from "./testAtoms";

export const actionInsertInput = atom(null, (get, set, data:number) => { // input 추가
    set(dataArr, (prev => [...prev, {id:data, content:''}]));
})

export const actionDeleteInput = atom(null, (get, set, data:number) => { // input 삭제
    const tempArr = get(dataArr);
    
    const copyTempArr:{id:number, content:string}[] = tempArr.filter((item) => item.id !== data);
    
    set(dataArr, copyTempArr);
})

export const actionUpdateInput = atom(null, (get, set, data:{idx:number; val:string;}) => { // input 수정
    const tempArr = get(dataArr);
    
    const copyTempArr:{id:number, content:string}[] = JSON.parse(JSON.stringify(tempArr));
    const index = copyTempArr.findIndex((item) => item.id === data.idx);
    copyTempArr[index].content = data.val;
    
    set(dataArr, copyTempArr);
})

 

둘 다 export를 통해 원하는 컴포넌트에서 사용할 수 있다. 앞서 익혔던 action을 이용해 input창을 동적으로 추가, 삭제, 수정을 하는 action을 만들어준다.

action을 사용할 땐 get을 통해 가져올 atom을 정의해 주고 가져온 atom을 원하는 로직을 통해 작업을 한 뒤 set에 해당 atom 이름을 정의하고 작업한 데이터를 넣어준다.

 

// jotaiTest.tsx
import { useAtom, useAtomValue } from "jotai";
import { useRef } from "react";

import {dataArr} from "./testAtoms";
import {actionInsertInput, actionDeleteInput, actionUpdateInput} from "./testActions";

const JotaiTest = () => {
    const inputRef = useRef<number>(0);

    const inputData = useAtomValue(dataArr);
    const [, setInsertInput] = useAtom(actionInsertInput);
    const [, setDeleteInput] = useAtom(actionDeleteInput);
    const [, setUpdateInput] = useAtom(actionUpdateInput);

    const actionAdd = ():void => {
        setInsertInput(inputRef.current+1);
        inputRef.current += 1;
    }

    return (
        <div>
            <center>
                <h1>Input 추가하기</h1>
            </center>

            <br/><br/><br/>

            {inputData.map((item, idx) => (
                <div key={idx}>
                    <input type="text" value={item.content} id={item.id+""} onChange={(e) =>setUpdateInput({idx:item.id, val:e.target.value})}/>
                    {
                        idx == 0 ? 
                            <button onClick={() => actionAdd()}>추가</button>
                            :
                            <button onClick={() => setDeleteInput(item.id)}>삭제</button>
                    }
                </div>
            ))}
        </div>
    )
}

export default JotaiTest;

 

내가 작성한 action들은 모두 set 동작만 정의했기에 set 동작만 할 수 있게 위처럼 작성해 주었고 뿌려줄 데이터는 useAtomValue를 통해 가져온다.

위에 작성한 코드들을 이해하면 Jotai의 동작 방식을 좀 더 쉽게 흡수하고 이해할 수 있을 것이다.

 

Zustand가 아닌 2번째로 사용해 보는 상태 관리 도구이다. 참고할 자료가 적다 보니 Zustand를 입문할 때 보다 좀 더 시간이 걸렸었지만 완벽하게 이해하고 나선 Zustand보다 좀 더 사용하기가 쉬운 것 같기도 하다.

앞으로도 유용하게 사용할 것 같다.

 

 

 

참고 사이트

 

https://devblog.kakaostyle.com/ko/2022-01-13-2-jotai-recipe/

 

Jotai 레시피

이번 글에서는 카카오스타일에서 Jotai를 어떤 식으로 사용하고 있는지 여러가지 패턴에 대해 설명하려고 합니다.

devblog.kakaostyle.com

 

https://medium.com/@clockclcok/recoil-%EC%9D%B4%EC%A0%9C%EB%8A%94-%EB%96%A0%EB%82%98-%EB%B3%B4%EB%82%BC-%EC%8B%9C%EA%B0%84%EC%9D%B4%EB%8B%A4-ff2c8674cdd5

 

Recoil, 이제는 떠나 보낼 시간이다

개요

medium.com

 

관련 포스팅

 

2024.02.27 - [[개인프로젝트] 개발 공부] - [프로젝트] 4. React 상태 관리 라이브러리 - Zustand

 

[프로젝트] 4. React 상태 관리 라이브러리 - Zustand

이전 다크모드를 구현할 때 사용했던 전역상태 관리 라이브러리 Zustand에 대해 알아보자. React의 상태관리 라이브러리는 다양하게 존재한다. 대표적으로 Context API, Redux, Recoil, MobX 등 많기도 하다.

rlawo32.tistory.com

 

 

 

 

 

 

 

 

728x90
반응형