상태 관리 회고
여러 단계의 parent/child 컴포넌트로 이루어진 애플리케이션을 만들다보면 상태 관리와 전달을 고민하게 됩니다.
애플리케이션 전체에서 공통으로 참조하고 변경하는 상태를 흔히 application level state 라고 합니다.
Redux가 없었을 때에는 event emitter 등으로 application level state를 컴포넌트간에 전달하고 forceUpdate
같은 것을 호출 했을 겁니다.
Redux는 상태 관리를 action, reducer, connect 등으로 패턴화 하였습니다.
네 Redux는 일종의 코드를 이렇게 작성하자는 약속과 패턴의 영역이라고 생각합니다.
그러다 보니, redux 방식은 생각보다 많은 코드를 작성해야 합니다.
또한 reducer라는 것은 컴포넌트 단에 드러나지 않아 코드를 이해하고 결과를 예측하기 위해 여러 파일을 추적해야 합니다.
그래서 저는 reducer에서 별도의 변환 로직을 넣지 않고 오로지 새로운 객체에 값을 복사하는 방식으로만 사용하는 편 입니다.
솔직히 딱히 다른 대안이 없기에 redux 패턴을 사용했습니다.
하지만, 하나의 상태를 추가히기 위해 action을 정의하고 connect하는 과정은 지루한 작업입니다.
막연하게 무언가 부족하다고 느껴질 때 즈음 recoil.js를 만나게 됩니다.
action 대신 atom
을 사용하고, reducer 대신 selector
를 사용하여 reactive한 프로그래밍을 할 수 있습니다.
atom
- 컴포넌트간 상태 공유
컴포넌트사이의 상태 공유는 간단히 atom
을 정의하면 됩니다.
값만을 사용할 때에는 useRecoilValue
를 변경까지 할 경우에는 useRecoilState
를 사용합니다.
너무 간단해서 더 설명할 것이 없습니다.
참조: Atoms | Recoil
selector
- reactive programminig
흔히들 엑셀의 동작과 비교하는 reactive programming 기법이 있습니다.
엑셀의 SUM
함수를 사용하면 여러 셀의 값을 더하여 그 결과를 표시합니다.
이후 SUM
함수로 지정한 셀 중의 하나를 수정하면 바로 합산 결과를 보여줍니다.
애플리케이션 사용자는 이러한 방식의 동작을 기대합니다.
selector
는 recoil state를 get
이라는 함수로 참조하는 것으로 observe할 수 있습니다.
Event emitter나 observer 같은 방식보다 직관적인 코드로 derives한 값을 정의할 수 있습니다.
async API 호출
로그인 사용자 정보는 다양한 컴포넌트에서 필요합니다.
여러 곳에서 사용하더라도 API 호출을 한번만 하고 싶습니다.
먼저 selector
로 promise를 반환하도록 아래와 같이 작성합니다.
const userState = selector({
key: 'userState',
get: async () => axios.get<UserInfo>('/login-user-info').then((r) => r.data),
});
Header
에서도 사용하고
function Header() {
const userInfo = useRecoilValue(userState)
return (
...
)
}
UserInfoPage
컴포넌트에서도 사용합니다.
function UserInfoPage() {
const userInfo = useRecoilValue(userState)
return (
...
)
}
한 화면에 Header
, UserInfoPage
컴포넌트를 동시에 mount해도 한번의 API만 호출합니다.
Reactive programming
Reactive programming은 어떻게 구현할 수 있는지 보겠습니다.
// 현재 화면에 표시할 post의 id
const currentPostIdState = atom({
key: 'currentPostIdState',
default: 0,
});
// post 정보
const currentPostState = selector({
key: 'currentPostState',
get: async ({ get }) => {
const currentPostId = get(currentPostIdState);
return axios.get<Post>(`/posts/${currentPostId}`).then((r) => r.data);
},
});
// post에 달린 comment 목록
const currenPostCommentsState = selector({
key: 'currenPostCommentsState',
get: async ({ get }) => {
const currentPostId = get(currentPostIdState);
return axios
.get<Comment[]>(`/posts/${currentPostId}/comments`)
.then((r) => r.data);
},
});
위의 코드는 currentPostIdState
를 변경할 경우 currentPostState
, currenPostCommentsState
를 새로운 API 호출과 함께 갱신합니다.
on
함수를 사용한 callback 처리나 observe를 사용한 chaining function 없이도 매우 직관적이고 간결한 코드로 reactive programming을 할 수 있습니다.
조금 복잡하지만 매우 유용한 예제를 살펴 보겠습니다.
Comment
가 10개 일지라도 작성자는 2명일 수 있습니다.
그리고 여러분은 작성자 정보를 가져오는 API 호출 횟수를 줄이고 싶을 겁니다.
먼저 Comment
의 userId
에 따른 UserInfo
값을 가지는 ResolvedComment
를 다음과 같이 정의합니다.
type Comment = {
userId: string; // 커멘트 작성자
comment: string; // 커멘트 내용
};
type ResolvedComment = Comment & {
user: UserInfo; // `userId`로 부터 가져온 사용자 정보
};
selectorFamily
를 사용하면 파라미터를 받을 수 있는 state를 정의할 수 있습니다.
파라미터는 serializable 즉 JSON.stringify 가능해야 합니다.
아마도 recoil.js 내부에서 캐쉬 key로 사용할 것 같습니다.
waitForAll
과 함께 사용하면 마법과 같은 코딩을 할 수 있습니다.
// userId 값으로 사용자 정보를 가져옵니다.
const userFamilyState = selectorFamily({
key: 'userFamilyState',
get: (userId: string) => async () => {
return axios.get<UserInfo>(`/user/${userId}`);
},
});
// 커멘트 목록 정보를 업데이트하면 커멘트 작성자 정보와 함께 업데이트 합니다.
const currentResolvedCommentsState = selector({
key: 'currentResolvedCommentsState',
get: ({ get }) => {
const comments = get(currenPostCommentsState);
// 각 comment의 작성자 정보를 매번 가져오는 것 같지만, selectorFamily로 인해 중복 호출을 하지 않습니다.
const users = get(
waitForAll(comments.map(({ userId }) => userFamilyState(userId)))
);
return comments.map((comment) => ({
...comment,
user: findItem(users, 'id', comment.useId),
}));
},
});
이제 currentPostIdState
만 변경하면 중복 호출 없이 각 커멘트의 작성자 정보까지 업데이트하는 간결하고 직관적인 라이브러리를 확보하였습니다.
원하는 recoil state를 원하는 컴포넌트에 useRecoilValue
로 hook하면 끝 입니다.
마치며
무언가를 완벽히 알고 사용하기 보다는, 필요한 것을 찾아 사용하는 편 입니다.
그래서 아직 recoil.js의 기능을 모두 아는 것은 아닙니다.
하지만, 다양한 요구사항에 대한 해결 방법을 고민했을 때, atom
과 selector
라는 이 간단한 개념들이 얼마만큼 확장가능한지 매번 놀라고 있습니다.
이런 새로운 개념으로 인해 금방이라도 legacy가 되어 버릴 redux나 mobx 기반의 코드 작성을 잠시 멈추시고, recoil.js를 사용해 보시는 것 어떨까요?
추천 드립니다.