본문 바로가기
C.W.K.
Stream
Lesson 03 of 08 · published

Custom Hooks + forwardRef 패턴

~14 min · react, hooks, patterns

Level 0호기심
0 XP0/69 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete

Custom hook 은 'advanced' 가 아니야 — 정리 방법이지

관련 hook 둘을 한 곳 이상에서 같이 쓴다 싶은 순간 추출해. 그게 규칙 전부야. 함수 이름 앞에는 use를 붙여. 그래야 React lint가 hook으로 알아보고, 조건문이나 루프 안에서 호출하는 실수를 잡아.

내 chat sidebar 엔 useConversations() hook. 내 input 엔 useCommandPalette() hook. 내 theme 시스템엔 useTheme() hook. 어느 것도 'advanced' 아니야 — 패턴 반복되니까 자연스럽게 추출한 거지.

forwardRef — parent 가 child DOM 노드 필요할 때

예전 React에선 ref가 일반 prop처럼 흐르지 않았어. parent가 child input의 focus를 제어하려면 forwardRef가 필요했지. React 19에선 function component가 ref를 prop으로 받을 수 있어서 새 코드는 wrapper 없이 같은 경계를 만들 수 있어. 아래 예제는 기존 코드에서 마주칠 패턴으로 읽어.

useImperativeHandle — custom ref API 노출

가끔 parent에게 raw DOM 노드를 주기 싫을 때가 있어. focus()scrollToBottom()처럼 허용한 동작만 노출하고 DOM 계약은 감추고 싶은 경우지. useImperativeHandle을 쓰면 child가 ref의 모양을 결정해.

언제 쓰나: React를 충분히 ship해서 이 도구가 풀어주는 고통을 직접 느끼기 전엔 거의 안 써. 첫 도구로 손이 간다면 view layer에 imperative 상태를 너무 많이 박은 거야.

Custom hook도 같은 절제가 필요해. 이름만 그럴듯한 useThing 안에 API 호출, 전역 상태, DOM 조작을 몽땅 숨기면 재사용이 아니라 책임 은폐야. 입력과 반환값을 보고 그 hook이 무엇을 소유하는지 말할 수 있어야 해. parent가 child에게 명령해야 할 때도 먼저 declarative prop으로 풀 수 있는지 봐. ref API는 마지막 좁은 문이지, 뒤에서 아무거나 만지는 비밀 통로가 아니야.

Code

useTheme — 실제 ThemeProvider 의 custom hook·tsx
type Theme = 'dark' | 'light' | 'system';

function useTheme() {
  const [theme, setTheme] = useState<Theme>(() => {
    return (localStorage.getItem('theme') as Theme) ?? 'system';
  });

  useEffect(() => {
    const resolved = theme === 'system'
      ? matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
      : theme;
    document.documentElement.dataset.theme = resolved;
    localStorage.setItem('theme', theme);
  }, [theme]);

  return { theme, setTheme };
}
InputArea with forwardRef — parent can focus the textarea·tsx
interface InputAreaHandle { focus: () => void }

const InputArea = forwardRef<InputAreaHandle, Props>((props, ref) => {
  const textareaRef = useRef<HTMLTextAreaElement>(null);
  useImperativeHandle(ref, () => ({
    focus: () => textareaRef.current?.focus(),
  }), []);
  return <textarea ref={textareaRef} {...props} />;
});

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.