[React 안티패턴 파훼하기] 커링을 이용한 콜백함수 내부의 hook 실행순서 보장

pengooseDev·2024년 3월 24일
0
post-thumbnail

들어가기 전, 컨텍스트 이해하기.

1. 하나의 View Model은 AtomManager라는 추상클래스을 상속받는다.

이는 상태관리 컨벤션을 확립하기 위함이다.
구현체는 아래와 같다.

import { Atom, WritableAtom, atom } from 'jotai';

export abstract class AtomManager<T> {
  public initialState: T;
  protected atom: WritableAtom<T, any, void>;

  constructor(initialState: T) {
    this.initialState = initialState;
    this.atom = atom(this.initialState);
  }

  abstract selectors: {
    [K in keyof Partial<T>]: Atom<T[K]>;
  };

  abstract actions: {
    [key: string]: WritableAtom<T | null, any, void>;
  };
}

각 selectors는 getter atom을 프로퍼티로 갖는다.
각 actions는 setter atom을 프로퍼티로 갖는다.

예시


class CartManager extends AtomManager<Cart> {
  constructor(initialState: Cart) {
    super(initialState);
  }

  /* Selectors */
  public selectors = {
    items: atom((get) => get(this.atom).items),
  };

  /* Actions */
  public actions = {
    add: atom(
      null,
      (get, set, { amount, product }: { amount: number; product: Product }) => {
        const { items } = get(this.atom);
        // ...codes
      }
    ),
    
    delete: atom(null, (get, set, product: Product) => {
      const { items } = get(this.atom);
      // ...codes
    }),
  };
}

const initialData: Cart = {
  items: [],
};

export const cartManager = new CartManager(initialData);

2. 고차함수를 이용해 Atom을 hook으로 래핑하고 타입을 동적으로 추론한다.

useManager은 두 가지 역할을 수행해야한다.

  1. 추상클래스로 추상화한 field(selectors, actions)가 가진 method Type들을 동적으로 추론해 반환해야한다.
  2. atom을 useAtomValue 등의 훅을 사용하여 state로 변환해야 한다.
export const useManager = <T extends AtomManager<any>>(manager: T) => {
  const selectors = Object.fromEntries(
    Object.entries(manager.selectors).map(([key, atom]) => [
      key,
      useAtomValue(atom),
    ])
  ) as {
    [P in keyof T['selectors']]: T['selectors'][P] extends Atom<infer V>
      ? V
      : never;
  };

  const actions = Object.fromEntries(
    Object.entries(manager.actions).map(([key, actionAtom]) => [
      key,
      useSetAtom(actionAtom),
    ])
  ) as {
    [P in keyof T['actions']]: T['actions'][P] extends WritableAtom<
      any,
      infer U,
      void
    >
      ? (param: U[0]) => void
      : never;
  };

  return { selectors, actions };
};

문제점 발생 - Callback 함수 내부에서 커스텀훅 호출

현재 코드는 콜백함수 내부에서 커스텀훅을 호출하여, hook의 호출 순서를 보장받지 못한다.

export const useManager = <T extends AtomManager<any>>(manager: T) => {
  const selectors = Object.fromEntries(
    Object.entries(manager.selectors).map(([key, atom]) => [
      key,
      useAtomValue(atom), // 🚩 Error : Callback 내부에서 커스텀훅 호출
    ])
  ) as {
    [P in keyof T['selectors']]: T['selectors'][P] extends Atom<infer V>
      ? V
      : never;
  };

  const actions = Object.fromEntries(
    Object.entries(manager.actions).map(([key, actionAtom]) => [
      key,
      useSetAtom(actionAtom), // 🚩 Error : Callback 내부에서 커스텀훅 호출
    ])
  ) as {
    [P in keyof T['actions']]: T['actions'][P] extends WritableAtom<
      any,
      infer U,
      void
    >
      ? (param: U[0]) => void
      : never;
  };

  return { selectors, actions };
};

커스텀훅은 최상위 Layer에서만 호출되어야한다.
Proxy를 써서 파훼를 시도했지만 역시 실패하였다.


해결 : 또 다시 고차함수(커링)

고차함수는 답을 알고있다. 커링을 응용해 props를 전달하는 함수를 반환하는 고차함수를 작성해주자.
최상위 layer에서 Hook을 사용할 수 있도록 함수로 래핑하여 반환해주자.

import { Atom, WritableAtom, useAtomValue, useSetAtom } from 'jotai';
import { AtomManager } from '@/Model/manager/atomManager';

const createUseSelector = <T>(atom: Atom<T>) => {
  return () => useAtomValue(atom); // ✅
};

const createUseAction = (atom: WritableAtom<any, any, void>) => {
  return () => useSetAtom(atom); // ✅
};

export const useManager = <T extends AtomManager<any>>(manager: T) => {
  const selectors = Object.fromEntries(
    Object.entries(manager.selectors).map(([key, atom]) => [
      key,
      createUseSelector(atom)(), // ✅
    ])
  ) as {
    [P in keyof T['selectors']]: T['selectors'][P] extends Atom<infer V>
      ? V
      : never;
  };

  const actions = Object.fromEntries(
    Object.entries(manager.actions).map(([key, actionAtom]) => [
      key,
      createUseAction(actionAtom)(), // ✅
    ])
  ) as unknown as {
    [P in keyof T['actions']]: T['actions'][P] extends WritableAtom<
      any,
      infer U,
      void
    >
      ? (param: U[0]) => void
      : never;
  };

  return { selectors, actions };
};

개선하여 @pengoose/jotai V1.1.4 배포! 😆👍


ThxTo

  • 고차함수 함수 및 고차 컴포넌트 설계에 통찰력을 주신 NextStep 그리고 소인성님 감사합니다.

0개의 댓글