[Recoil selector 만져보기]와 [Recoil selector로 비동기 작업 수행하기]를 통해 selector란 무엇이고, 어떻게 활용할 수 있는지 알아봤다. 이번에는 selector를 사용했을 때 렌더링 최적화 관점에서 유리한 부분을 한번 알아보려고 한다.
유저 데이터를 담은 state를 선언하고 각각 다른 컴포넌트에서 해당 유저 데이터를 참조한다고 해보자. user state는 name과 age를 담은 object 형태이고, Name 컴포넌트에서는 name을, Age 컴포넌트 에서는 age를 참조하고 수정할 수 있다. 이 때 Name 컴포넌트에서 useState의 name 필드를 수정하면 Age 컴포넌트는 리렌더링 될까?
//store.js
export const userState = atom({
key: "userState",
default: {
name: "kim",
age: "15",
},
});
//Name.js
const Name = () => {
console.log("Name rendering");
const [user, setUser] = useRecoilState(userState);
return (
<div>
{user.name}
<button onClick={() => setUser({ ...user, name: "lee" })}>
이름 바꾸기
</button>
</div>
);
};
export default Name;
//Age.js
const Age = () => {
console.log("Age rendering");
const [user, setUser] = useRecoilState(userState);
return (
<div>
{user.age}
<button onClick={() => setUser({ ...user, age: 26 })}>나이 바꾸기</button>
</div>
);
};
export default Age;
당연하게도 user의 name 필드를 수정하니 Age 컴포넌트까지 리렌더링이 이루어지고 있고, 그 반대도 성립했다. Name과 Age 컴포넌트 모두 user state를 참조하고 있고, name 필드를 수정하기 위해서는 user state를 직접 변경해야 하니 user state를 참조하고 있던 Age 또한 리렌더링 되는 것이다.
Age 컴포넌트 입장에서는 오직 age 필드에 의해서만 그 내용이 달라지기 때문에 name의 변경으로 인해 렌더링 되는 것은 굉장히 불필요한 작업이고, 최적화를 위해서는 반드시 방지해야 하는 단계이기도 하다. 그리고 너무나 다행스럽게도 selector를 이용하면 간단히 이 문제를 해결할 수 있다.
//store.js
export const nameState = selector({
key: "nameState",
get: ({ get }) => get(userState).name,
set: ({ get, set }, name) => set(userState, { ...get(userState), name }),
});
export const ageState = selector({
key: "ageState",
get: ({ get }) => get(userState).age,
set: ({ get, set }, age) => set(userState, { ...get(userState), age }),
});
//Name.js
const Name = () => {
console.log("Name rendering");
const [name, setName] = useRecoilState(nameState);
return (
<div>
{name}
<button onClick={() => setName("lee")}>이름 바꾸기</button>
</div>
);
};
export default Name;
//Age.js
const Age = () => {
console.log("Age rendering");
const [age, setAge] = useRecoilState(ageState);
return (
<div>
{age}
<button onClick={() => setAge(26)}>나이 바꾸기</button>
</div>
);
};
export default Age;
이전에는 userState를 가져와 필요한 요소를 user.name 형태로 가져다 썼다면, 이번에는 selector를 활용해 user의 필드만 각각 가져와서 사용하고 있다. 본래 selector가 atom을 참조하고, atom이 변경되면 selector도 함께 동기화 되기 때문에 아까와 달라진 점이 없어 보이지만, 실제로 실행을 해보면...
렌더링 최적화가 이루어졌다. 아까와 다르게 user의 name 필드를 변경했을 때 age 필드를 참조하고 있는 Age 컴포넌트에서 리렌더링은 발생하지 않았다. 서로 다른 selector가 같은 atom을 참조하고, setter의 결과로 원본 atom에 변화가 있더라도, 또다른 selector가 참조하고 있는 필드가 변경되지 않았다면 recoil이 이를 판단해서 영리하게 리렌더링을 일으키지 않는 것이다.
따라서 이렇게 name selector가 age 필드까지 변경한다면, Age 컴포넌트 또한 이를 감지해서 리렌더링 되게 된다.
//store.js
export const nameState = selector({
key: "nameState",
get: ({ get }) => get(userState).name,
set: ({ set }, name) => set(userState, { name, age: 26 }),
});
...Sexy...(실제로 감탄하면서 내뱉은 말...)
selector를 사용하면 렌더링 최적화에 메리트가 있다는 것은 알았다. 하지만 이런 생각이 들수도 있다.
만약 userState의 필드가 많아진다면 어떻게 될까...?
user = {
name:"kim",
age:"15",
sex:"male",
job:"student",
hobby:"tennis",
...
}
모든 필드에 대해 각각의 selector를 작성하는 것은 코드 재사용성이나 유지보수 관점에서 좋지 못하다. 따라서 이럴 때는 selectorFamily를 이용하는 것을 고려할 수 있다. (selectorFamily 사용 방법에 대해서는 [Recoil의 atomFamily와 selectorFamily 사용해보기]를 봐주세요.)
//store.js
export const userDataState = selectorFamily({
key: "userDataState",
get:
(params) =>
({ get }) =>
get(userState)[params],
set:
(params) =>
({ get, set }, newValue) =>
set(userState, { ...get(userState), [params]: newValue }),
});
//Name.js
...
const [name, setName] = useRecoilState(userDataState("name"));
...
//Age.js
...
const [age, setAge] = useRecoilState(userDataState("age"));
...
잘 작동한다.
글이 참 맛있네요 ^^ 잘 먹고 갑니다