class hook과 데코레이터에 대해 #247
Replies: 1 comment 1 reply
-
안녕하세요. 먼저 eslint-plugin-react-hooks 버전이 최신인지 확인해보시면 좋을 것 같습니다. 저 역시 트리키한 코드에 호기심이 많지만, React가 제시한 Hook 규칙을 지키는 편이 장기적으로 훨씬 이득이라고 생각해요. 앞으로 도입될 React Compiler와 같은 도구는 “규칙 준수”를 전제로 설계되기 때문에, 규칙을 우회해서 얻을 수 있는 이점보다 안정성에 대한 리스크가 더 클것 같아요. 말씀하신 것 처럼 데코레이터로 횡단 관심사를 분리하는 것도 좋은 패턴이라 생각합니다. 이런식으로 접근해보시면 어떨까 싶네요. import { useSyncExternalStore } from 'react';
class ToggleStore {
private on = false;
private listeners = new Set<Listener>();
toggle = () => {
this.on = !this.on;
this.emit();
};
getSnapshot = () => this.on;
subscribe = (listener: Listener) => {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
};
private emit() {
this.listeners.forEach((l) => l());
}
}
const toggleStore = new ToggleStore();
export function useToggle() {
const on = useSyncExternalStore(toggleStore.subscribe, toggleStore.getSnapshot);
return {
on,
toggle: toggleStore.toggle,
};
} |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
안녕하세요. 디자인 패턴에 관심이 많은 프론트엔드 개발자 ayden이라고 합니다. 리액트를 다뤄본 적 있는 개발자라면 누구에게나 커스텀 훅의 개념은 익숙할 겁니다. 하지만 class hook은 아주 낯설고, 누군가에게는 아예 처음 들어보는 종류의 개념일 거라 생각합니다.
class hook
3월의 어느날에 ─ 여느 때와 다름 없이 코드로 차력쇼를 하다가 ─ 저는 이상한 코드를 하나 작성했습니다. 왜 이런 코드를 작성해야겠다고 생각했는지는 기억나지 않지만, 일단 작성하고나자 굉장히 모순적인 상황에 빠지게 되었습니다.
이 코드를 작성하고 나서 가장 먼저 든 생각은 "어? 클래스 안에서 훅을 호출했네?"였습니다. 리액트를 조금이라도 깊게 다뤄본 개발자라면, 훅은 컴포넌트 함수 또는 커스텀 훅 함수의 최상단에서만 호출해야 한다는 규칙을 잘 알고 있을 겁니다. 위 코드는 그 규칙을 정면으로 위배하고 있죠. 그럼에도 불구하고 ESLint는 어떠한 에러도 내뱉지 않았습니다. 에러만 없던 게 아닙니다. 위 코드는 아래와 같이 문제 없이 호출되어 동작합니다.
저는 이러한 ─ 훅의 규칙을 위배하면서도 문제가 없는 ─ 현상이 발생하는 이유를 완벽히 설명해낼 자신은 없습니다. 다만, JS에서 클래스는 생성자 함수로 전환되어 처리되는데, 그로 인해 리액트가 이러한 class hook을 커스텀 훅 함수와 구분하지 못한다는 가설 정도만 가지고 있습니다. 리액트 파이버노드를 열어보면 이 가설에 힘을 실어주는 몇 가지 근거를 찾을 수 있지만, 이에 대해서 더 이야기하지는 않겠습니다. 당장 저에게 중요한 것은 '이게 된다'는 것이었으니까요.
class hook의 불편함
며칠 클래스 훅을 사용해보며 제가 느낀 가장 큰 불편함이 하나 있었는데, 이는 this binding과 연관된 문제였습니다. 가령 일반적인 커스텀 훅 함수를 사용할 경우 저는 아래와 같이 코드를 짜는 편입니다.
하지만 class hook은 this를 통해 맴버 변수에 접근하는 까닭에 항상 해당 메서드나 프로퍼티를 호출할 때 this 컨텍스트가 보존되어야 합니다. 즉, toggle과 같은 메서드를 이벤트 핸들러에 그대로 넘기면 this가 undefined가 되거나 예상과 다른 객체를 참조하게 되죠. 이를 방지하기 위해 toggle.bind(this)를 사용하거나, 아예 화살표 함수로 정의해야 하는 번거로움이 따릅니다.
이건 함수형 컴포넌트 안에서 훅을 사용하는 직관적인 방식과는 거리가 있는 방식이며, React의 사용성을 해친다는 인상을 줍니다. 결국 클래스 훅은 훅의 규칙을 우회하려는 실험적 시도일 수는 있어도, 실전에서의 DX는 오히려 악화될 수 있다는 이야기를 듣기도 했습니다.
데코레이터와 class hook
아직 JS에 공식적으로 추가된 문법은 아니지만, TS를 사용한다면 tsconfig.json에서 "experimentalDecorators" 옵션을 통해 데코레이터 문법을 사용할 수 있습니다. 이러한 문법이 익숙하지 않은 분들이 많을 수도 있겠지만, 제가 참 좋아하는 백엔드 프레임워크인 Nest.js에서는 이미 이러한 데코레이터를 본격적으로 사용하고 있습니다. 자바/코틀린의 스프링부트를 써보셨다면 '어노테이션'이 데코레이터와 비슷한 개념입니다.
데코레이터는 클래스의 선언, 메서드, 필드 등에 기능을 주입하거나 수정할 수 있도록 해주는 특수한 문법입니다. 일종의 메타프로그래밍 도구로, 코드에 부가적인 의미나 동작을 부여하는 데 사용됩니다. JavaScript/TypeScript에서는 이 데코레이터가 클래스 기반 구조에만 적용됩니다. 함수나 변수 같은 일반 구조에는 사용할 수 없고, 오직 클래스 자체 또는 클래스의 구성 요소들(메서드, 필드, 접근자 등)에 한정됩니다.
데코레이터를 활용하면 this binding과 같은 문제에서 자유로워질 수 있습니다. 덕분에 아래와 같이 class hook의 인스턴스를 구조분해할당해도 메서드 내부의 this는 여전히 원본 class hook을 가리키게 됩니다.
관심 있는 분들을 위해 thisBind 데코레이터의 코드 전문을 아래에 첨부합니다.
데코레이터의 활용
class hook의 this binding을 고정시키는 것 외에도 데코레이터는 class hook과 함께 다양한 일을 처리할 수 있습니다. 가령 아래의 transformResult 데코레이터의 경우 메서드가 리턴하는 데이터 인터페이스를 간단히 변경해줍니다. 일종의 어댑터 패턴인 셈인데, 프로젝트 초기에 백엔드 인터페이스가 자주 ─ 아주 자주 ─ 바뀌는 경우 굉장히 유용합니다.
물론 데코레이터 없이도 가능한 일이지만, 여러 도메인에서 동일한 작업을 여러번 반복하는 것보다 데코레이터 하나 만들어서 적용하는 것이 훨씬 편합니다.
이처럼 데코레이터는 횡적 관심사를 분리하는 데 탁월한 도구입니다. 로깅, 에러 핸들링, 결과 변환, 접근 제어 등과 같이 비즈니스 로직과는 직접 관련 없지만 여러 메서드에 공통으로 적용되어야 하는 로직들을 데코레이터로 추출해 두면, 코드의 가독성과 유지보수성이 크게 향상됩니다. 특히 클래스 훅처럼 상태와 로직이 객체 안에 응집되어 있는 구조에서는 이러한 데코레이터 패턴이 더욱 빛을 발합니다. 반복되는 보일러플레이트를 줄이고, 도메인 로직은 본연의 목적에 더 집중할 수 있도록 도와주는 것이죠.
위의 코드처럼 메서드 내에서 커스텀 훅 함수를 호출하는 일이 굉장히 낯설 것으로 생각됩니다만, 아무 문제 없이 동작합니다(멤버 변수에 커스텀 훅 함수를 호출할 때와 달리 메서드 내에서 커스텀 훅 함수를 호출하는 경우 ESLint가 react-hooks/rules-of-hooks 에러를 내뱉기는 합니다. 하지만 실제 동작에는 아무런 문제가 없습니다). 이는 리액트 쿼리의 메인테이너인 TkDodo가 남겨준 짧은 코멘트를 통해서도 알 수 있습니다.
결론
저는 class hook과 데코레이터를 결합하는 이러한 시도가 리액트의 함수형 중심 구조 속에서도 객체지향적인 감각을 녹여낼 수 있는 흥미로운 접근이라고 생각합니다.
물론, 현재 리액트 훅의 규칙은 함수형 컴포넌트와 최상위 수준의 호출에 맞춰 설계되어 있기 때문에, 클래스나 메서드 내부에서의 훅 호출은 규칙을 어기는 것으로 간주되어 lint 에러가 발생합니다. 하지만 이와 같은 시도는 훅의 기능을 객체지향 패턴과 융합하려는 실험적 도전으로서 의미가 있으며, 실제로 동작에도 문제없다는 점에서 향후 더 넓은 가능성을 보여줍니다.
이 방식이 공식적으로 인정받기 위해서는 훅의 규칙을 우회하거나 보완하는 새로운 레이어(예: Babel 플러그인, transform 툴) 가 필요할 수 있겠지만요 :)
class hook과 데코레이터의 결합에 대한 여러분의 생각은 어떤지 궁금합니다!
Beta Was this translation helpful? Give feedback.
All reactions