React-hook-form 부셔보기 : useSubscribe()

서혁준·2022년 7월 17일
0
post-thumbnail

글 쓰는 방식을 실제 코드를 뜯어보는 과정과 유사하게 작성했다. 그래서 뭔가 딱딱 정리되어 있는 글 보다는 확실히 읽기 어려울 수 있지만, 같이 코드의 내용을 고민해 보면서 생각의 과정을 따라와 본다면 많은 도움이 될 거라고 생각한다!

📌 TL:DR;

useForm.ts를 읽다 보면 등장하는 useSubscribe custom hook이 어떤 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]);
}
  1. 인터페이스
	type Props<T> = {
	  disabled?: boolean;
	  subject: Subject<T>;
	  callback: (value: T) => void;
	};

우선 useSubscribe의 인터페이스부터 살펴보자. disabled, subject, callback을 받고 있다.

  1. useRef
	const _props = React.useRef(props);
	  _props.current = props;

내부적으로 보면 parameter로 받아온 PropsuseRef 를 이용해서 상태를 저장해두고 있다.

  1. useEffect
    로직을 확인해보면 현재 subscription을 unsubscribe하도록 하는 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,
  };
}

위에서부터 천천히 보다보면 굉장히 재밌는 코딩 컨벤션들이 많이 나온다. 보고 배우기 딱 좋은 내용이다.

  1. 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를 보자.

  1. observers
	let _observers : Observer<T>[] = [];

변수 네이밍에서부터 배울점이 있다. 내부의 상태를 관리하는 변수 앞에는 _ 를 붙여서 사용한다는 것이다. 별 거 없어보이지만, 이런 컨벤션 하나하나가 쌓여서 좋은 코드를 만든다고 생각한다.

  1. next()
	const next = (value: T) => {
		for (const observer of _observers) {
		  observer.next(value);
		}
	  };

observers 배열을 순회하면서 내부의 observer 객체들의 .next() 메서드를 호출하는 함수이다.

  1. 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 할 수 있는 메서드를 반환한다.

  1. unSubscribe()
	const unsubscribe = () => {
		_observers = [];
	  };

현재 subscribe중인 모든 observer를 한번에 비우는 (unsubscribe) 함수 를 정의하고 있다.

  1. return
	return {
		get observers() {
		  return _observers;
		},
		next,
		subscribe,
		unsubscribe,
	  };

return statement를 보면 굉장히 재밌는 표현이 있는데, 바로 get 이다.

일반적으로 이런 getter, setter 패턴은 클래스에서 사용하는데, Object에서 사용할 수 있는줄은 몰랐다.
링크 에서 이에 대해서 다음과 같이 설명하고 있다.

get 구문은 객체의 속성 접근 시 호출할 함수를 바인딩합니다.

이렇게 Object 에서 getter에 함수를 바인딩하는 것은 ES2015 이후에 도입한 것이라고 한다. 내용을 찾아보니 다양한 접근자를 바인딩 할 수 있다고 하는데 나중에 프로젝트에서 사용해 보면 매우 좋을 것 같다.

정리

그러면 뜯어본 내용을 가지고 createSubject 함수가 뭐하는 함수인지 생각해보자.

인터페이스만 본다면 어떤 Subject라는 타입을 가지는 객체를 생성하는 함수이다.

Subject 객체는

  1. 현재 subscribe 중인 observer의 목록인 observers,
  2. 새로운 observer를 obsrevers에 추가할 수 있는 subscribe 함수,
  3. 그리고 observers목록을 비울 수 있는 unsubscribe 함수,
  4. 현재 subscribe 중인 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을 SubjectObserver로써 등록하고, 그 결과로 해당 Observer에 대한 Subscription 객체를 저장하고 있다는 것을 확인할 수 있다.

const tearDown = (subscription: Subscription | false) => {
      if (subscription) {
        subscription.unsubscribe();
      }
    };
...
return () => tearDown(subscription);

위에서 받은 Subscription 객체에 대해서 subscription을 해제하는 tearDown함수를 정의하고, 이를 cleanup function으로 등록했다.

그렇다면 useSubscribe hook이 무슨 일을 하는걸까?

어떤 Subject에 대해서 등록하고 싶은 callback을 넘겨줘서 disabled 상태에 따라서 해당 callbackSubjectObserver로서 등록하고, 해제하는 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

profile
방구석에 앉아 미래를 상상하는 나

0개의 댓글