Next13 블로그 프로젝트

Jotai를 프로젝트에 적용해보며 느낀점

sakuraop 2023. 11. 20. 23:54

1. Jotai Atom의 핵심은 Atomic이다.

Jotai는 useContext에서 발생하는 리렌더링 문제를 해결하기 위해 개발되었다.

=> 여기서 리렌더링 문제란 하나의 상태라도 변경이 되면 context가 선언된 컴포넌트의 하위 모든 컴포넌트가 리렌더링 된다.

 

예를 들면 다음과 같다.

import React, { createContext, useContext, useState } from 'react';

const MyContext = createContext();

// 1. 부모 컴포넌트에서 context를 생성
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  return (
    <MyContext.Provider value={count}>
      <자식1 />
      <자식2 />
    </MyContext.Provider>
  );
};

// 2. 자식1 컴포넌트에서 context로 구독한 값을 변경
const 자식1 = () => {
  const [, setCount] = useContext(MyContext);
  const handleClick = () => {
    setCount(prevCount => prevCount + 1);
  };
  return <button onClick={handleClick}>Increase Count</button>;
};

// 3. 자식2는 변경된 상태가 없지만 부모 컴포넌트의 상태가 변경되었기 때문에 리렌더링 됨
const 자식2 = () => {
  return <p>자식2</p>;
};

부모 컴포넌트에서 상태를 모두 가지고 Provider로 하위 컴포넌트에서 구독하여 해당 상태를 이용하거나 변경하는데, 자식에서 상태를 변경시키면 부모 컴포넌트의 상태가 변경되기 때문에 모든 자식이 리렌더링 되는 것이다.

 

Jotai는 이 문제를 다음과 같이 해결할 수 있다.

=> atom을 전역으로 선언하거나 store에 선언한다. => atom을 컴포넌트 끝단으로 전달한다.

import React, { ReactNode } from 'react';
import { Provider, atom, createStore, useAtom } from 'jotai';

// 1. store 성생
const store = createStore();
const isModalVisibleAtom = atom<boolean>(false);

// 2. Provider로 store를 참조하도록 함
export const SearchPostProvider = ({ children }: { children: ReactNode }) => {
  return <Provider store={store}>{children}</Provider>;
};

// 3. store에 상태를 저장
export const useIsModalVisibleAtom = () => {
  return useAtom(isModalVisibleAtom, { store });
};


const SearchPost = () => {
  return (
    <SearchPostProvider>
      <SearchModal />
      <OpenSearchModalButton />
      <자식1/>
      <자식2/>
      <자식3/>
    </SearchPostProvider>
  );
};

이렇게 Provider를 생성하게 되면 SearchPost는 부모임에도 상태를 가지고 있지 않게 된다.

따라서 OpenSearchModalButton으로 SearchModal의 isModalVisible 여부를 조절할 수 있다.

 

context를 이용하였더라면 SearchPost의 상태가 변경되어 상태가 변경되지 않은 자식1,2,3 도 같이 리렌더링 되는 상황에서

=> atom을 이용하면 SearchModal과 OpenSearchModalButton만 리렌더링 되는 결과를 가져올 수 있게 된다.

 

이러한 두 방식의 차이는 각각 다음과 같이 표현된다.

context : top-down 방식

jotai : bottom-up 방식

 

atomic한 컴포넌트를 만들고, 이들을 조합하면서 점점 큰 컴포넌트를 구성해가는 atomic design pattern에 곁들이기 좋은 이유이다.

2. Atom은 파생 설계만 잘하면 코드 가독성이 매우 좋아진다.

Jotai 레퍼런스의 example과 tutorial을 살펴보면 derived atom 이라 하여 "파생"부터 가르친다.

파생이란 atom의 값을 가지고 또 다른 atom을 만들어 나가는 방식을 말한다.

 

간단한 예를 들면 다음과 같다.

// 기본 atom 생성
const counterAtom = atom(0);

// 파생된 atom 생성 (atomFamily 사용)
const doubledCounterAtom = atomFamily((id) => (get) => {
  const count = get(counterAtom);
  return count * 2;
});

doubledCounterAtom은 counterAtom에서 파생됐다.

 

counterAtom의 값이 1이 증가하면

=> doubledCounterAtom의 값은 2가 된다.

 

doubledCounterAtom은 counterAtom에 의존하고 있기 때문에

counterAtom의 값이 변경될 때 doubledCounterAtom이 함께 변경된다.

 

척 보면 알겠는데 그래서 이걸 어떻게 하는가가 포인트다.

 

가령 todoList를 atom으로 생성한다고 해보자.

const todos = atom([{todo:"", completed:false}])

const completedTodos = atom((get) => get(todos).filter(todo => todo.completed))

todos에서 파생된 completedTodos를 이용하여 완료된 todo 배열을 만들었다.

 

기본적으로 todos를 렌더링 하고 있다가,

completed에 체크 하면 completedTodos를 렌더링하도록 하는 방식으로 손쉽게 필터링 기능을 구현할 수 있다.

 

이를 활용해서 게임 채팅 필터링 기능을 한 번 생각해 보자,

["전체", "길드", "파티"] 세가지의 채팅 기능이 있다.

const chatsAtom = atom([chat,chat,chat])
const filterAtom = atom("all")

const targetChatsAtom = atom((get) => get(chatsAtom).filter(chat => {
  const filter = get(filterAtom)
  
  return get(chatsAtom).filter(chat => chat.target === filter)
})

const useChangeFilter = (target:string) => {
  setfilterAtom(target)
}

useChangeFilter("party") 

=> 파티채팅만 모아서 보여주기

 

이러한 방식으로 채팅창에서 필터링할 메시지를 골라내는 기능을 구현한다.

props를 전달하지 않고 atom 파일 내에 구현하게 되고

atom에서 파생되는 atom과 이를 다루는 함수들을 컴포넌트의 끝단에 전달한다.

 

filter 컴포넌트 내에서 useChangeFilter를 import하여 컨트롤하는 방식으로 가져와서 쓰는 방식으로

컴포넌트의 리렌더링을 최소화할 수 있다. 

 

 

 

https://github.com/pmndrs/jotai/discussions/2044

 

Sharing state with library component · pmndrs/jotai · Discussion #2044

Hey all. First of all let me say thanks for an amazing library. On the current project I faced the following problem. We have separate npm package with react components. They are imported by core a...

github.com