글 쓰는 방식을 실제 코드를 뜯어보는 과정과 유사하게 작성했다. 그래서 뭔가 딱딱 정리되어 있는 글 보다는 확실히 읽기 어려울 수 있지만, 같이 코드의 내용을 고민해 보면서 생각의 과정을 따라와 본다면 많은 도움이 될 거라고 생각한다!
useForm.ts
를 읽다 보면 등장하는 useSubscribe
custom hook이 어떤 hook인지 코드를 톺아보면서 파악해본다.
useForm.ts
useForm.ts
내부를 보다보면 다음과 같은 코드를 발견할 수 있다.
useSubscribe({
subject: control._subjects.state,
callback,
});
도데체 이게 무슨 코드지? 라는 생각으로 해당 hook의 구현을 뜯어보기로 했다.
일단 무슨 코드인지는 잘 모르겠지만 props로 주어지는걸 보면 어떤 subject
가 주어지고, 이것의 변화(?) 에 따라서 callback
을 실행하는 게 아닐까? 하는 생각을 해볼 수 있다.
useSubscribe.ts
import React from 'react';
import { Subject, Subscription } from './utils/createSubject';
type Props<T> = {
disabled?: boolean;
subject: Subject<T>;
callback: (value: T) => void;
};
export function useSubscribe<T>(props: Props<T>) {
const _props = React.useRef(props);
_props.current = props;
React.useEffect(() => {
const tearDown = (subscription: Subscription | false) => {
if (subscription) {
subscription.unsubscribe();
}
};
const subscription =
!props.disabled &&
_props.current.subject.subscribe({
next: _props.current.callback,
});
return () => tearDown(subscription);
}, [props.disabled]);
}
type Props<T> = {
disabled?: boolean;
subject: Subject<T>;
callback: (value: T) => void;
};
우선 useSubscribe
의 인터페이스부터 살펴보자. disabled
, subject
, callback
을 받고 있다.
const _props = React.useRef(props);
_props.current = props;
내부적으로 보면 parameter로 받아온 Props
를 useRef 를 이용해서 상태를 저장해두고 있다.
tearDown(Subscription)
함수를 정의하고,subscription
이라는 변수 / 객체를 정의한다. subscription
에는 현재 hook의 disabled
옵션이 true
일 경우에는 false
가 담기고, false
일 경우에는 subject.subscribe()
의 결과 객체가 담긴다.
그리고 useEffect의 cleanup function으로 tearDown
을 사용하고 있다.
여기까지 봤을 때 아직 이게 무슨 hook인지는 잘 모르겠다. Subject
라는 객체가 어떤건지 알아야 감이 올 것 같다.
이 subject
객체와 Subscription
이라는 타입은 어떻게 정의되어 있을지 보러 가보자.
createSubject.ts
import { Noop } from '../types';
export type Observer<T> = {
next: (value: T) => void;
};
export type Subscription = {
unsubscribe: Noop;
};
export type Subject<T> = {
readonly observers: Observer<T>[];
subscribe: (value: Observer<T>) => Subscription;
unsubscribe: Noop;
} & Observer<T>;
export default function createSubject<T>(): Subject<T> {
let _observers: Observer<T>[] = [];
const next = (value: T) => {
for (const observer of _observers) {
observer.next(value);
}
};
const subscribe = (observer: Observer<T>): Subscription => {
_observers.push(observer);
return {
unsubscribe: () => {
_observers = _observers.filter((o) => o !== observer);
},
};
};
const unsubscribe = () => {
_observers = [];
};
return {
get observers() {
return _observers;
},
next,
subscribe,
unsubscribe,
};
}
위에서부터 천천히 보다보면 굉장히 재밌는 코딩 컨벤션들이 많이 나온다. 보고 배우기 딱 좋은 내용이다.
Noop
타입 import { Noop } from '../types';
이걸 보고 도데체 뭐지…? 싶었다. noob도 아니고 이게 뭐지..? 그래서 타입 정의를 타고 들어가봤더니
export type Noop = () => void;
라고 정의되어 있었다. No Operation의 준말이었던 것.
사실 이런 타입의 경우 () => void
라고 정의해도 괜찮지만, 이렇게 Noop이라는 이름을 붙여줌으로써 좀 더 의미있는 코드를 짤 수 있다.
비슷한 예시로 NextAuth.js 라이브러리에서 YYYY-MM-DDTHH:mm:ss.sssZ
와 같은 형식의 ISO 문자열에 대해서 ISODateString
이라는 타입을 정의해서 사용하고 있는데 찾아가 보면 다음과 같이 정의되어 있다.
export type ISODateString = string;
이제 createSubject
를 보자.
observers
let _observers : Observer<T>[] = [];
변수 네이밍에서부터 배울점이 있다. 내부의 상태를 관리하는 변수 앞에는 _
를 붙여서 사용한다는 것이다. 별 거 없어보이지만, 이런 컨벤션 하나하나가 쌓여서 좋은 코드를 만든다고 생각한다.
next()
const next = (value: T) => {
for (const observer of _observers) {
observer.next(value);
}
};
observers
배열을 순회하면서 내부의 observer
객체들의 .next()
메서드를 호출하는 함수이다.
subscribe()
const subscribe = (observer: Observer<T>): Subscription => {
_observers.push(observer);
return {
unsubscribe: () => {
_observers = _observers.filter((o) => o !== observer);
},
};
};
인터페이스를 보면 Observer
객체를 받아서 Subscription
객체를 반환하는 함수이다.
내용을 보면 _observers
배열에 주어진 Observer
객체를 push하고, 해당 Observer
에 대해서 unsubscribe
할 수 있는 메서드를 반환한다.
unSubscribe()
const unsubscribe = () => {
_observers = [];
};
현재 subscribe중인 모든 observer를 한번에 비우는 (unsubscribe) 함수 를 정의하고 있다.
return
return {
get observers() {
return _observers;
},
next,
subscribe,
unsubscribe,
};
return statement를 보면 굉장히 재밌는 표현이 있는데, 바로 get
이다.
일반적으로 이런 getter
, setter
패턴은 클래스에서 사용하는데, Object에서 사용할 수 있는줄은 몰랐다.
링크 에서 이에 대해서 다음과 같이 설명하고 있다.
get
구문은 객체의 속성 접근 시 호출할 함수를 바인딩합니다.
이렇게 Object
에서 getter에 함수를 바인딩하는 것은 ES2015 이후에 도입한 것이라고 한다. 내용을 찾아보니 다양한 접근자를 바인딩 할 수 있다고 하는데 나중에 프로젝트에서 사용해 보면 매우 좋을 것 같다.
정리
그러면 뜯어본 내용을 가지고 createSubject
함수가 뭐하는 함수인지 생각해보자.
인터페이스만 본다면 어떤 Subject
라는 타입을 가지는 객체를 생성하는 함수이다.
Subject
객체는
observers
, obsrevers
에 추가할 수 있는 subscribe
함수, observers
목록을 비울 수 있는 unsubscribe
함수,observer
들의 next()
메서드를 호출하는 next
함수,로 구성되어 있다.
결국 createSubject
함수는 여러 observer
들을 등록시켜 놓고 변경 등에 대한 effect들을 관리할 수 있는 Subject
객체를 만들어주는 함수라고 할 수 있다.
이제 이렇게 쌓은 createSubject
에 대한 이해를 기반으로 다시 useSubscribe
를 보자.
useSubscribe.ts
const subscription =
!props.disabled &&
_props.current.subject.subscribe({
next: _props.current.callback,
});
disabled
가 false인 경우에 주어진 callback을 Subject
에 Observer
로써 등록하고, 그 결과로 해당 Observer
에 대한 Subscription
객체를 저장하고 있다는 것을 확인할 수 있다.
const tearDown = (subscription: Subscription | false) => {
if (subscription) {
subscription.unsubscribe();
}
};
...
return () => tearDown(subscription);
위에서 받은 Subscription 객체에 대해서 subscription을 해제하는 tearDown
함수를 정의하고, 이를 cleanup function으로 등록했다.
그렇다면 useSubscribe
hook이 무슨 일을 하는걸까?
어떤 Subject
에 대해서 등록하고 싶은 callback
을 넘겨줘서 disabled
상태에 따라서 해당 callback
을 Subject
에 Observer
로서 등록하고, 해제하는 custom hook이라는 것을 알 수 있다.
useSubscribe({
subject: control._subjects.state,
callback,
});
이제 이 코드가 어떤 일을 하는지 말할 수 있다. useForm
안에서 사용중인 control
객체의 _subjects
중에 state
라는 subject
에 대해서 지정한 callback
을 등록하는 커스텀 훅인 것이었다!
React-hook-form 라이브러리
https://react-hook-form.com/
자바스크립트 접근자
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Functions/get