전역 상태 관리 Recoil

박민우·2024년 1월 9일
1

🚨 Recoil

목록 보기
1/1
post-thumbnail

📌 상태 관리의 중요성

리액트는 상태를 단방향으로 바인딩을 하는 라이브러리입니다. 부모에서 자식 방향으로만 stateprops로 전달할 수 있고, 자식의 props를 부모에게 전달하는 방법은 존재하지 않습니다.

자식 component에서 부모 componentstate를 바꿀 수 있는 방법에는 2가지가 존재합니다.

  1. 자식에게 부모의 state를 변경할 수 있는 setState 함수를 props로 넘겨준다.
  2. 상태 관리 도구(redux, recoil 등)을 사용한다.

하지만 1번 방법은 서비스의 규모가 조금이라도 커지게 된다면 상태 관리에 어려움을 겪을 수 있습니다. 즉, 자식을 내려보내는 depth가 조금이라도 깊어지면 1번과 같은 방법은 효율적이지 않습니다.


React 자체를 통해서도 상태 관리가 가능하지만, 다음과 같은 한계가 존재합니다.

  • 컴포넌트의 상태는 공통된 상위요소까지 끌어올려야만 공유될 수 있으며, 이 과정에서 거대한 트리가 다시 렌더링되는 효과를 야기하기도 합니다.
  • Context는 단일 값만 저장할 수 있으며, 자체 소비자(consumer)를 가지는 여러 값들의 집합을 담을 수는 없습니다.
  • 위 두 가지 특성이 트리의 최상단(state가 존재하는 곳)부터 트리의 말단(state가 사용되는 곳)까지의 코드 분할을 어렵게 합니다.

전역 상태 관리 도구인 Recoil을 사용하면 atoms(공유 상태)에서 selectors(순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있습니다.


📌 atom

Recoil에서 atom이란, 상태의 단위이고 업데이트와 구독이 가능합니다.

  • 상태의 단위이고, 값의 업데이트가 가능합니다.

    => useState 훅과 비슷한 느낌이라고 생각할 수 있습니다.

  • 구독이 가능합니다.

    => 모든 컴포넌트가 전역적으로 상태를 공유할 수 있습니다.

    => atom이 업데이트 되면 이를 구독한 각각의 컴포넌트는 새로운 값을 반영하여 리렌더링이 일어나게 됩니다.

    => 동일한 atom을 여러 컴포넌트에서 구독하고 있다면 모든 컴포넌트는 그 상태를 공유합니다.

  • atom은 런타임에 생성될 수도 있고, React의 로컬 컴포넌트의 상태 대신 사용할 수 있습니다.


atom 정의 및 사용 예시

다음은 atom을 정의하는 예시입니다.

import { atom } from 'recoil';

// atom 생성 
export const todoListState = atom({ 
	key: 'todoListState',
	default: []
});

export const todoListFilter = atom({
    key: 'todoListFilterState',
    default: 'Show All',
})
  • atom 이라는 함수를 사용해서 atom을 생성할 수 있습다.

  • atom 함수에 인자로 key와 상태가 가질 기본 값 default를 넣어줘야 합니다.

  • atom의 키는 전역적으로 고유해야 합니다.

    => 2개의 atom이 같은 키를 가지면 오류를 일으킵니다.


다음은 atom을 사용하는 예시입니다.
// atom을 사용하는 컴포넌트 파일 
import { todoListState } from 'store/atom'

const TodoList = (props) => {
	const [todoList, setTodoList] = useRecoilState(todoListState); // atom 구독
	
    const handleClickSetTodoList = (newList) => {
        setTodoList(newList);
    };
    
    return (
    	<>
        	{todoList.map((todoItem) => (
    			<TodoItem key={todoItem.id} item={todoItem}/>
        	))}
		</>
	);
};

위의 코드의 useRecoilState 훅처럼 atom의 값에 접근하고 변경하기 위해 사용할 수 있는 러가지 hook들을 알아보겠습니다.

useRecoilState

const [todoList, setTodoList] = useRecoilState(todoListState); 

atom으로 정의된 state를 읽고 쓰기 위해서는 useRecoilState라는 훅을 사용할 수 있습니다.

  • useState 처럼 사용하고

    => 인자에 atom 혹은 selector를 넣어 [state, setState]같은 튜플에 할당한다.

  • 반환되는 배열의 첫 번째 인자 todoList는 값을 읽을 때 사용하는 값이고

  • 두번째 인자 setTodoList는 atom 값을 변경할 수 있는 함수이다.


useRecoilValue

const todoList = useRecoilValue(todoListState);

만약 상태를 사용하는 컴포넌트에서 atom이나 selector의 상태는 변경하지 않고 값을 읽는 용도로만 사용한다면 useRecoilValue 훅을 사용할 수 있습니다.

이 hook은 상태를 읽을 수만 있게 하고 싶을 때 사용하는 것을 추천합니다.


useSetRecoilState

const setTodoList = useSetRecoilState(todoListState);

상태의 setter 함수만을 활용하기 위해 사용됩니다. 선언된 변수에 setter 함수를 할당하여 사용합니다.

💡 useRecoilState를 사용하면 될텐데 useSetRecoilState가 필요한 이유 ??

const [todoList, setTodoList] = useRecoilState(todoListState);

위와 같이 useRecoilState를 사용해 다음과 같이 받아오면 todoList까지 같이 받아오기 때문에 이 atom을 구독하게 됩니다. 따라서 todoList의 값이 바뀌면 이 컴포넌트는 다시 렌더링됩니다.

const setTodoList = useSetRecoilState(todoListState);

위와 같이 useSetRecoilState를 사용하면 해당 atom을 구독하지 않고 해당 atom을 업데이트만 할 수 있어서, 불필요한 렌더링을 일으키지 않습니다.


useResetRecoilState

const resetTodo = useResetRecoilState(todoListState);

전역상태를 초기(deafult) 값으로 reset하기 위해서 사용됩니다. 선언된 함수 변수에 할당하여 사용합니다.


📌 selector

  • selector는 기존의 상태인 atom이나 selector에서 파생될 수 있는 data에 접근할 수 있게 해줍니다.

  • selector는 atom나 다른 selector를 입력으로 받아들이는 순수 함수입니다.

  • 상위에 있는 atom 또는 selector가 업데이트 되면 하위의 selector 함수도 다시 실행됩니다.

  • selector를 atom과 마찬가지로 구독할 수 있고 selector가 변경될 때도 컴포넌트는 리렌더링됩니다.


selector를 사용하는 이유

=> 최소한의 상태 단위만 atoms에 저장하고 그 상태를 기반으로 파생되는 모든 데이터들은 selector에 명시한 함수를 통해서 효율적으로 계산함으로써 쓸모없는 상태의 보존을 방지합니다.

EX)

todoList도 필요하고, 특정 filter가 적용된 todoList도 필요하다면

=> 두 개의 state를 atom으로 각각 가지고 있는 것보다는, todoList라는 atom을 한 번만 선언하고 filter가 적용된 todoList를 계산해서 반환해주는 selector를 정의하는 쪽이 더 효과적입니다.

단, 지금 상황처럼 특정 filter가 적용된 todoList가 todoList에서 파생될 수 있기 때문에 selector를 사용하는 것이 가능한 것입니다.


사용 예시

기존에 존재하는 atom에 접근해 이로부터 파생된 새로운 state에 접근할 수 있는 selector를 만들어보겠습니다.

// atom 선언 
export const todoListState = atom({
	key: 'todoListState',
	default: []
});

export const todoListFilter = atom({
    key: 'todoListFilterState',
    default: 'Show All',
})
export const filteredTodoListState = selector({
    key: 'filteredTodoListState',
    get: ({get}) => {
        const filter = get(todoListFilterState);
        const list = get(todoListState); 
        
        switch(filter){
            case 'A': 
                return list.filter((item)=>item.isComplete);
            case 'B':
                return list.filter((item)=>!item.isComplete);
            default:
                return list;
        }
    }
})
  • selector 함수를 호출해 인자로 keyget을 넣어서 selector를 생성합니다.

  • key : selector 마다 고유한 값으로 설정해줘야 합니다.

  • get

    • 함수를 정의합니다. 여기서 정의해주는 함수의 리턴값이 실제로 이 selector를 호출했을 때 계산되어서 접근할 수 있는 값이 됩니다.

    • 함수의 인자로 {get}을 받아 이 get 함수를 사용해 다른 atomselector에 접근할 수 있습니다. 이렇게 참조했던 다른 atom 혹은 selector가 업데이트 되면 이 get 함수도 다시 실행됩니다.

      const filter = get(todoListFilterState); // atom에 접근 
      const list = get(todoListState); // atom에 접근 
    • 위 예시의 filteredTodoListState 라는 selector는 todoListFilterStatetodoListState라는 두개의 atom에 의존성을 가지고 있습니다.

  • 이렇게 정의된 selector의 값에 접근하기 위해서는 useRecoilValue 훅을 사용할 수 있습니다.

    const filteredTodoList = useReocilValue(filteredTodoListState);

    현재 filteredTodoListState 라는 selector는 writable하지 않기 때문에 useRecoilState를 사용하지 않았습니다.

    selector를 정의할 때, get 함수만 정의한다면 selector는 읽기만 가능한 RecoilValueReadOnly 객체를 반환합니다. 그렇기 때문에 읽기 전용 훅은 useRecoilValue를 사용한 것이고, 만약 selector에서 set 함수를 정의한다면 쓰기도 가능한 useRecoilState 훅을 사용할 수 있습니다.


🙇🏻‍♂️ 참고

Recoil 알아보기
Recoil 200% 활용하기

profile
꾸준히, 깊게

0개의 댓글