리액트는 상태를 단방향으로 바인딩을 하는 라이브러리입니다. 부모에서 자식 방향으로만 state
를 props
로 전달할 수 있고, 자식의 props
를 부모에게 전달하는 방법은 존재하지 않습니다.
자식 component에서 부모 component의 state
를 바꿀 수 있는 방법에는 2가지가 존재합니다.
- 자식에게 부모의
state
를 변경할 수 있는setState
함수를props
로 넘겨준다.- 상태 관리 도구(redux, recoil 등)을 사용한다.
하지만 1번 방법은 서비스의 규모가 조금이라도 커지게 된다면 상태 관리에 어려움을 겪을 수 있습니다. 즉, 자식을 내려보내는 depth가 조금이라도 깊어지면 1번과 같은 방법은 효율적이지 않습니다.
React 자체를 통해서도 상태 관리가 가능하지만, 다음과 같은 한계가 존재합니다.
전역 상태 관리 도구인 Recoil을 사용하면 atoms(공유 상태)에서 selectors(순수 함수)를 거쳐 React 컴포넌트로 내려가는 data-flow graph를 만들 수 있습니다.
Recoil에서 atom이란, 상태의 단위이고 업데이트와 구독이 가능합니다.
상태의 단위이고, 값의 업데이트가 가능합니다.
=> useState 훅과 비슷한 느낌이라고 생각할 수 있습니다.
구독이 가능합니다.
=> 모든 컴포넌트가 전역적으로 상태를 공유할 수 있습니다.
=> atom이 업데이트 되면 이를 구독한 각각의 컴포넌트는 새로운 값을 반영하여 리렌더링이 일어나게 됩니다.
=> 동일한 atom을 여러 컴포넌트에서 구독하고 있다면 모든 컴포넌트는 그 상태를 공유합니다.
atom은 런타임에 생성될 수도 있고, React의 로컬 컴포넌트의 상태 대신 사용할 수 있습니다.
다음은 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을 사용하는 컴포넌트 파일
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들을 알아보겠습니다.
const [todoList, setTodoList] = useRecoilState(todoListState);
atom
으로 정의된 state를 읽고 쓰기 위해서는 useRecoilState
라는 훅을 사용할 수 있습니다.
useState
처럼 사용하고
=> 인자에 atom 혹은 selector를 넣어 [state, setState]
같은 튜플에 할당한다.
반환되는 배열의 첫 번째 인자 todoList
는 값을 읽을 때 사용하는 값이고
두번째 인자 setTodoList
는 atom 값을 변경할 수 있는 함수이다.
const todoList = useRecoilValue(todoListState);
만약 상태를 사용하는 컴포넌트에서 atom이나 selector의 상태는 변경하지 않고 값을 읽는 용도로만 사용한다면 useRecoilValue
훅을 사용할 수 있습니다.
이 hook은 상태를 읽을 수만 있게 하고 싶을 때 사용하는 것을 추천합니다.
const setTodoList = useSetRecoilState(todoListState);
상태의 setter 함수만을 활용하기 위해 사용됩니다. 선언된 변수에 setter 함수를 할당하여 사용합니다.
💡 useRecoilState를 사용하면 될텐데 useSetRecoilState가 필요한 이유 ??
const [todoList, setTodoList] = useRecoilState(todoListState);
위와 같이 useRecoilState를 사용해 다음과 같이 받아오면
todoList
까지 같이 받아오기 때문에 이 atom을 구독하게 됩니다. 따라서todoList
의 값이 바뀌면 이 컴포넌트는 다시 렌더링됩니다.const setTodoList = useSetRecoilState(todoListState);
위와 같이 useSetRecoilState를 사용하면 해당 atom을 구독하지 않고 해당 atom을 업데이트만 할 수 있어서, 불필요한 렌더링을 일으키지 않습니다.
const resetTodo = useResetRecoilState(todoListState);
전역상태를 초기(deafult) 값으로 reset하기 위해서 사용됩니다. 선언된 함수 변수에 할당하여 사용합니다.
selector는 기존의 상태인 atom이나 selector에서 파생될 수 있는 data에 접근할 수 있게 해줍니다.
selector는 atom나 다른 selector를 입력으로 받아들이는 순수 함수입니다.
상위에 있는 atom 또는 selector가 업데이트 되면 하위의 selector 함수도 다시 실행됩니다.
selector를 atom과 마찬가지로 구독할 수 있고 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
함수를 호출해 인자로 key
와 get
을 넣어서 selector를 생성합니다.
key
: selector 마다 고유한 값으로 설정해줘야 합니다.
get
함수를 정의합니다. 여기서 정의해주는 함수의 리턴값이 실제로 이 selector를 호출했을 때 계산되어서 접근할 수 있는 값이 됩니다.
함수의 인자로 {get}
을 받아 이 get
함수를 사용해 다른 atom
과 selector
에 접근할 수 있습니다. 이렇게 참조했던 다른 atom 혹은 selector가 업데이트 되면 이 get
함수도 다시 실행됩니다.
const filter = get(todoListFilterState); // atom에 접근
const list = get(todoListState); // atom에 접근
위 예시의 filteredTodoListState
라는 selector는 todoListFilterState
와 todoListState
라는 두개의 atom에 의존성을 가지고 있습니다.
이렇게 정의된 selector의 값에 접근하기 위해서는 useRecoilValue
훅을 사용할 수 있습니다.
const filteredTodoList = useReocilValue(filteredTodoListState);
현재
filteredTodoListState
라는 selector는 writable하지 않기 때문에useRecoilState
를 사용하지 않았습니다.selector를 정의할 때,
get
함수만 정의한다면 selector는 읽기만 가능한 RecoilValueReadOnly 객체를 반환합니다. 그렇기 때문에 읽기 전용 훅은useRecoilValue
를 사용한 것이고, 만약 selector에서 set 함수를 정의한다면 쓰기도 가능한useRecoilState
훅을 사용할 수 있습니다.