본문으로 건너뛰기

hook은 생각하고 만드세요.....제발

· 약 17분

hooks가 react에 도입되면서 class 없이 상태와 여러 react의 기능을 사용하게 해주었다. hooks의 도입으로 react에 신세계가 열렸고, custom hooks라는 것도 만들기 시작했다. 그런데, hooks... 생각하고 만들고 있나요?

시작에 앞서...

이 포스트의 내용은 해당 PR reviewer들의 진심 어린 조언과 생각을 담아 정리한 것입니다. 이와 같은 조언을 해 주시고 이 포스트 작성에 도움을 주셔서 정말 감사드립니다.😊

시작

PR을 하나씩 reslove해 가는 과정에서 한 가지 리뷰에 맞닥트렸습니다.

이미지

기능은 되도록이면 hook으로 분리하는 것이 이번 프로젝트의 컨벤션이었습니다. (뒤늦게 알았지만 팀 리더가 custom hooks에 익숙해지라는 의미에서 좀 더 기능을 hook으로 분리하기를 원해 했었습니다.)

그래서 저는 custom hooks를 어떻게 만드는지 찾아보았습니다. 그리고 각각의 프로젝트 파일에 작성된 상태들을 모두 묶어 custom hooks로 만들어주었습니다! 예를 들자면 아래와 같이 말이죠.

useRate.ts
import { useState } from 'react';

export interface StarRatingProps {
rating: () => void;
}

export default function useRate({ rating }: StarRatingProps) {
const [hover, setHover] = useState(0);
const [click, setClick] = useState(0);
const fixStarCount = (num: number) => {
setClick(num);
rating();
};
const countStarHover = (num: number) => {
setHover(num);
};
const starHoverLeave = (num: number) => {
setHover(num);
};
return {
hover,
click,
fixStarCount,
countStarHover,
starHoverLeave,
};
}

그리고 훅을 사용하는 파일에서 아래와 같이 선언해 사용했습니다.

const { hover, click, fixStarCount, countStarHover, starHoverLeave } = useRate({ rating });

이렇게 훅을 만들어 오던 사람이 있다면 반성합시다.(저도 같이...) 그리고 이 포스트를 끝까지 읽어주세요. 이 훅은 정말 말 그대로 상태를 통째로 훅에다가 집어 넣어 버린, 생각이 없는 custom hook입니다.

문제 찾기

제가 만든 이 훅의 문제를 더 자세히 들여다 봅시다. 감사하게도 이 훅의 문제가 무엇인지 여러 리뷰를 달아주셨고, 이 리뷰 내용들을 기반으로 이야기 해 보겠습니다.

정말 죄송하지만... 이렇게 만드실거면 굳이 훅을 만드는 의의가 없어보여요. useRate 라는 네이밍을 가진 훅을 호출하면 rate 나 rate 관련 메서드를 반환해야 할것 같은데 rate.hover, rate.fixStartCount, rate.countStarHover 같은 메서드가 하는 역할이 잘 상상이 안가요. 오히려 이 코드를 유지보수할 사람은 이 훅이 뭔지 다시 파악하는데 시간을 더 쓸 것 같아요. 사실 저는 이 컴포넌트를 만든다면 애초에 훅을 안 만들었을 것 같아요... 이전 로직이 가독성 면에서 훨씬 나아보여요.

커스텀 훅 패턴은 단순한 로직 분리로만 사용하시면 안됩니다. 이 커스텀 훅을 분리하는 것을 common으로 쓸지 이 컴포넌트에만 쓸지는 마음대로 정할 수 있지만 이 컴포넌트(UI)에 종속해서 사용하게 만들면 좋지 않습니다. 그래서 변수이름을 일반적으로 지을 것, 훅을 다른 곳에서 사용할 것을 리뷰하는 게 자주 코드리뷰에 출현하고 있습니다. 한 번 다시 생각해보고 코드를 검토해보시길 바랍니다.

useRate라는 훅을 호출하면 호출하는 사람은 이렇게 생각할 것입니다.

'이런 메서드가 있겠지?, 이런걸 반환 해줄거야!'

이 훅은 호출하는 사람의 예상을 모두 뒤엎습니다. 대부분의 메서드가 문제지만, 대표적으로 한 가지 메서드를 보자면 hover라는 메서드는 rate와는 정말 관련이 없어보입니다. 이런 훅은 코드를 직관적으로 볼 수 없게 합니다. 위의 리뷰에서도 언급되었다시피 오히려 이 hook이 뭔지 파악하는데 시간을 더 쓰게 될 것입니다. 그리고 두 번째 리뷰에서 알다시피 제 훅은 단순히 로직을 분리한 완벽한 예시라고 할 수 있겠네요.

Hook이 무엇인지 생각하기

그럼 hook은 어떻게 만들어야 할까요? 아니 일단 hook이 뭘까요? hook을 한국어로 직역하자면 갈고리가 되겠네요. 갈고리를 생각하면 배라는 이미지가 떠오릅니다. 어떤 배가 목적지를 향해 항로를 따라 가고 있습니다. 그런데 이 배는 짐도 함께 날라야 합니다. 그렇다면 이 배는 짐을 갈고리로 연결하고 항해하게 될 겁니다. 이 때 짐을 배의 갈고리에 거는 작업을 hooking 이라고 할 수 있겠네요. 저는 그래서 배를 컴포넌트라고 한다면 hook은 배와 함께하는 기능이 될 것 같다고 생각했습니다!

hook이라는 용어는 React 에서만 쓰이는 용어가 아닙니다. 대표적으로 웹 훅은 서버에게 특정 동작이 일어나면 클라이언트 측으로 데이터를 전달하도록 hooking 할 수 있습니다.

Hook을 왜 쓰는지 생각하기

리액트의 컴포넌트는 기본적으로 같은 prop 을 주입하면 내부 로직들을 실행하다가 return 문을 만나고 ReactElement 를 반환합니다. 이 과정에서 여러 종류의 상황에 대응하기 위해서는 컴포넌트 내부 코드 만으로 해결할 수 없는 경우가 생깁니다. hook은 이를 지원할 수 있습니다.

대표적인 react hooks는 무엇을 지원해주고 있을까요?

  • useState
    • 컴포넌트가 호출 될 때 상태가 필요합니다.
    • 이 상태는 컴포넌트에 활용될 수도 있습니다.
  • useEffect
    • 컴포넌트 내부 상태가 변경될 때마다 Side Effect 가 발생합니다.
    • 의존성과 callback 함수를 넘길테니 의존성이 변경될 때마다 callback 함수를 호출해줍니다.
  • useRef
    • 컴포넌트가 여러 번 호출되더라도 바뀌지 않는 동일한 reference 가 필요합니다.

React Component는 선언적인 패러다임을 따릅니다. Hook 또한 선언적이어야 합니다.

정보

선언형 프로그래밍?

선언형 프로그래밍은 "무엇" 에 대해 초점을 맞추는 프로그래밍 기법이라고 말할 수 있겠습니다. "무엇" 에 대해 초점을 맞췄기 때문에 같은 값을 주입하면 무엇을 반환하는지 예측할 수 있어야 합니다. 그렇기 때문에 같은 값에 대해 같은 결과가 나와야 합니다. 즉, 순수하게 작성되어야 합니다.

아시다시피 React Component에서 같은 Props 를 주입하면 같은 ReactElement 가 반환됨을 기대할 수 있습니다. Hook도 동일합니다. Hook에서도 같은 인자를 넣으면 같은 동작을 기대할 수 있어야 합니다.

좋은 Hook을 어떻게 작성할지 생각하기

위에서 이야기 했다시피 선언적인 훅은 순수하게 작성되는 것이 이상적입니다. 순수하다는 의미는 위에서도 언급했습니다. 같은 인자를 넣었을 때 같은 동작을 하기를 기대하고 이 동작을 했을 때 어떤 값을 가질 수 있고 어떤 메서드를 사용 수 있는지 기대할 수 있다는 의미입니다.

이렇게 훅을 작성하려면 어떻게 해야할까요? 이 시점에서 추상적으로 작성하려는 고민을 해야 합니다. 보다 추상적으로 코드를 만들 수록, 여러 모듈에서 사용할 수 있는 로직이 만들어집니다.

이 관점을 토대로 저는 좋은 훅에 관해 아래와 같은 의견을 얻을 수 있었습니다.

  • 컴포넌트 내부에 여러 관심사가 존재하는가?
    • 만약 그렇다면, 훅으로 관심사를 분리할 수 있다.
    • 그렇지 않다면 훅을 만듦으로써 오히려 가독성을 해칠 수도 있으니 고민을 해봐야 한다.
  • 훅에 여러 관심사가 존재하는가?
    • 만약 그렇다면, 별로 좋지 못한 훅이다. 훅은 선언적이여야 한다. 하나의 문맥을 가지도록 훅을 쪼갠다.
  • 다른 컴포넌트에서도 사용될 수 있도록 훅은 순수하게 만들어지는가?
    • 만약 그렇지 않다면 별로 좋지 못한 훅일 가능성이 있다.
    • Side Effect 는 컴포넌트에서 useEffect 로 감싸주는 것이 좋다고 생각한다.
  • 훅의 반환값 네이밍은 직관적으로 만들어질 수 있는가?
    • 네이밍이 직관적으로 잘 만들어지지 않는다면 관심사가 잘 드러나지 않아서 그럴 가능성이 크다.
    • 어설프게 훅을 만듦으로써 가독성을 해치는 것보단 훅을 만들지 않는것이 낫다.

여기에서 말하는 좋은 훅의 예시를 살펴봅시다. 아래는 useHistory라는 hook입니다.

const history = useHistory();

이 hook은 history.pop, history.push, history.replace와 같은 메서드를 사용할 수 있습니다.

history는 그 자체로 history라는 관심사 하나를 다룹니다. 그리고 history라는 주제는 특정 컴포넌트에 얽매이지 않고 여러 컴포넌트에서 쓰일 수 있을 것 같습니다. pop, push, replace라는 메서드의 네이밍도 정말 직관적입니다.

이렇게 좋은 훅이라고 부를 수 있는 훅은 위의 의견을 모두 만족하고 있네요!

이제 좋은 훅을 보았으니, 아래의 훅을 살펴보고 고쳐봅시다.

useCheckRate
export default function useCheckRate() {
const [isActive, setIsActive] = useState(false);
const setActive = () => {
setIsActive(true);
};
const setDeActive = () => {
setIsActive(false);
};
return {
isActive,
setActive,
setDeActive,
};
}

useCheckRate 훅은 단순히 setActive를 하면 상태를 true로, setDeActive를 하면 상태를 false로 바꾸어줍니다. 언뜻보면 알기 쉬울수도, 한 문맥에 있다고 생각이 듭니다. 이 생각까지는 맞는 것 같습니다. 하지만 문제는 이 훅은 이름이 useCheckRate라는 점입니다. 이 훅은 더욱 추상화가 가능한 구조에 있습니다. useCheckRate라는 이름은 이 가능성을 막고 있습니다. 그렇다면 이름은 제외하고 타입과 상태, 함수가 하는 역할만을 살펴볼까요?

  • boolean이라는 상태를 다룬다.
  • 상태를 true로 바꾸어준다.
  • 상태를 false로 바꾸어준다.

네, 이게 전부입니다. 그렇다면 boolean이라는 상태를 다루는 훅이니 useBooleanState는 어떨까요? 몇가지 메서드를 좀 더 추가하면 다음과 같이 만들 수 있을 겁니다.

useBooleanState
import { Dispatch, SetStateAction, useCallback, useState } from 'react';

interface ReturnType {
value: boolean;
setValue: Dispatch<SetStateAction<boolean>>;
setTrue: () => void;
setFalse: () => void;
toggle: () => void;
}

export default function useBooleanState(defaultValue?: boolean): ReturnType {
const [value, setValue] = useState(!!defaultValue);

const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
const toggle = useCallback(() => setValue((x) => !x), []);

return {
value,
setValue,
setTrue,
setFalse,
toggle,
};
}

어떤가요? 좋은 훅처럼 보이시나요? 단순히 boolean을 다룬다라는 문맥 안에서 다른 컴포넌트에서도 충분히 재사용 가능해 보입니다. 반환 값 네이밍도 이름 그 자체의 역할을 해주고 있습니다. useCheckRate라는 훅은 이제 더 이상 필요가 없게 되었습니다. 점수가 체크되었는지 구분하는 상태를 만들고 싶다면 이제 아래와 같이 만들 수 있습니다.

const isRateCheck = useBooleanState(false);
// 체크 되었다고 하면
isRateCheck.setTrue;
// 체크 안되었다고 하면
isRateCheck.setFalse;
// 특정 버튼을 누를 때마다 상태가 변한다고 하면
isRateCheck.toggle;

이렇게 특정 컴포넌트 내에서의 역할과 이름만 보기보다는 상태와 그 상태를 변경해주는 함수 자체에 관심을 가지고 살펴보면 추상화 할 수 있는 훅이 나오기도 합니다.

무조건적이지는 않다.

하지만 무조건 훅은 재사용 되어야만하고, 그 자체로 순수해야만 한다고 할 수는 없습니다. 실제로 가독성 면에서 선언적인 훅을 만듦으로써 문맥을 구분하는 방식은 현업에서도 자주 사용한다고 합니다. 대신에 이런 경우에는 상위 모듈이 아닌 최대한 낮은 hierarchy(계층) 에 훅을 만드려고 해야할 것입니다.