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

Custom Hooks + forwardRef 패턴

~14 min · react, hooks, patterns

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

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

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

내 chat 화면은 useChat() hook 위에서 돌아. 음성 재생은 useVoice() 위에서. 지우기 같은 파괴적인 버튼은 useArmedAction() 을 써 — window.confirm() 대신 두 번 눌러야 발사되는 장치야. 그리고 느린 작업은 전부 useElapsedSeconds() 로 째깍거리는 타이머를 띄워. 어느 것도 '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

useElapsedSeconds — cwkPippa 프론트엔드의 실제 hook·tsx
/** 느린 작업엔 멈춘 "Loading…" 대신 째깍거리는 경과 시간을 띄워 —
 *  안 그러면 "도는 중" 이랑 "멈춘 것" 이 똑같아 보여.
 *  시작 시각을 태그로 달아서 지난 실행의 값이 새어 들어오지 못하게. */
export function useElapsedSeconds(active: boolean): number {
  const [state, setState] = useState<{ start: number | null; value: number }>({
    start: null,
    value: 0,
  });

  useEffect(() => {
    if (!active) {
      setState({ start: null, value: 0 });
      return;
    }
    const start = Date.now();
    setState({ start, value: 0 });
    const id = setInterval(() => {
      setState({ start, value: Math.floor((Date.now() - start) / 1000) });
    }, 1000);
    return () => clearInterval(id);
  }, [active]);

  return state.start === null ? 0 : state.value;
}
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

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

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