주스탄드에서 get- 메소드 사용 금지

쑹춘·2023년 4월 19일
0

주스탄드와 게터

목록 보기
2/3

주스탄드에서 getter를 사용하기에서 계속하는 것인데,


함수로 작성을 하더라도 의존 상태가 변경될 때에 명시적으로 변경하지 않는다면, 함수 포인터는 이전과 같기 때문에 컴포넌트는 변경을 알아챌 수가 없다.


의 이유로 get- 형태의 함수를 사용하는 것이 무의미하다. 적어도 실수를 유발할 위험이 있다. 그런 의미에서,

import { create } from "zustand";

type NameStore = {
  firstName: string | null,
  lastName: string | null,
  getFullName: () => string,
};

const useNameStore = create((set, get) => ({
  firstName: null,
  lastName: null,
  getFullName() {
    const { firstName, lastName } = get();
    return [firstName, lastName].join(" "); 
  },
}));

const Component = () => {
  const getFullName = useNameStore(({ getFullName }) => getFullName);
  
  // firstName, lastName에 대한 의존성이 없기 때문에,
  // 두 상태의 변경이 있어도 Component는 재 렌더링 하지 않는다.  
  return null;
};

이것과 같은 결론을 내렸었는데, 아래와 같이 setter 메소드에서 사이드이펙트를 직접 작성하는 쪽으로 바꾸어야만 한다.

const useNameStore = create((set, get) => ({
  firstName: null,
  lastName: null,
  fullName: null,
  setFirstName(value: string) {
    const { lastName } = get();
    set({ firstName: value, fullName: [value, lastName].join(" ") });
  },
  setLastName(value: string) {
    const { firstName } = get();
    set({ lastName: value, fullName: [firstName, value].join(" ") });
  },
}));

당연히 전혀 마음에 들지 않는다! 어쩌면 subscribe를 사용해볼 수도 있다.

/* 주의: 이렇게 하면 안됩니다. */

const useNameStore = create((set, get) => ({
  firstName: null,
  lastName: null,
  fullName: null,
  setFirstName(value: string) {
    set({ firstName: value });
  },
  setLastName(value: string) {
    set({ lastName: value });
  },
}));

useNameStore.subscribe(
  ({ firstName, lastName }) => {
    useNameStore.setState({ fullName: [firstName, lastName].join(" ");
  }
);

하지만 스택이 오버플로난다. subscribe listener를 호출하는 setState를 내부에서 호출하고 있기 때문이다.

  1. 탈출 조건을 만든다. 이를테면 firstName, lastName의 previous 상태와 비교하여 바뀌었을 때에만 fullName을 set 한다. 혹은,
  2. Using subscribe with selector

먼저 2번째 방법은 여기를 읽고 ts 제너릭 인자를 잘 던져주어야 한다. 커링 처리도 한 번 하여야 한다.

type NameStore = {
  firstName: string | null;
  lastName: string | null;
  fullName: string | null;
};

const useNameStore = create<NameStore>()(
  subscribeWithSelector<NameStore>(() => ({
    firstName: null,
    lastName: null,
    fullName: null,
  }))
);

그리고 selector와 subscribe 처리를 아래와 같이 하여야만 한다.

useNameStore.subscribe(
  (state) => state.firstName,
  (firstName) => {
    const { lastName } = useNameStore.getState();
    useNameStore.setState({ fullName: [firstName, lastName].join(" ") });
  }
);

useNameStore.subscribe(
  (state) => state.lastName,
  (lastName) => {
    const { firstName } = useNameStore.getState();
    useNameStore.setState({ fullName: [firstName, lastName].join(" ") });
  }
);

// 혹은

useNameStore.subscribe(
  ({ firstName, lastName }) => ({
    firstName,
    lastName,
  }),
  ({ firstName, lastName }) => {
    useNameStore.setState({ fullName: [firstName, lastName].join(" ") });
  },
  {
    equalityFn: shallow, // zustand/shallow 폴더에 있다.
  }
);

아름답지 않은 것도 같다. 나는 문서를 조금 더 덜 읽고, 간결함 때문에 쥬스탄드를 고른 것인데,

1번으로 가보자. 탈출 조건을 만든다.

useNameStore.subscribe(
  (
    { firstName, lastName },
    { firstName: prevFirstName, lastName: prevLastName }
  ) => {
    if (firstName === prevFirstName && lastName === prevLastName)
      return;

    useNameStore.setState({ fullName: [firstName, lastName].join(" ") });
  }
);

(다소 지역 프로퍼티 이름이 장황할 순 있어도, 코파일럿의 도움을 받으면 된다.)

나는 아래쪽을 조금 더 선호한다.

물론 가장 선호하는 것은 subscribe에 이러한 부작용 코드를 쓰는 게 아니라, 상태를 선언하는 곳에 getter 처럼 선언하는 것이다. vue에선 computed 같은 걸로 할 수 있다. 이렇게 보면 기반 수준에서는 react는 여전히 절차적procedural인 면모가 있는 것 같다.

zustand 대장인 다이시 씨는 이러한 수요는 아예 jotai에서 해소하기로 한다. 조타이는 일견 주스탄드의 대용체처럼 쓰기엔 시작점이 다르다는 생각이 들어버리는데, 무엇보다 조타이는 react 바깥에서 쓰려면 다소 돌아가야 하는 부분이 치명적이었는데, 아무래도 주스탄드의 단순함을 지키고 싶었기 때문이 아닐까 싶다.

profile
성천

0개의 댓글