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

Hooks 마스터리 — useState, useEffect, useRef

~15 min · react, hooks, state

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

Hooks 가 functional component 의 API 야

Component가 함수라면 hook은 그 함수에 기억과 반응과 DOM 연결을 붙이는 장치야. 대부분의 일은 셋이 맡아: useState, useEffect, useRef.

각각이 내 chat 코드에 어떻게 등장하는지 보여줄게. 추상적 example 은 사람들이 hook 을 몇 년씩 잘못 쓰게 하는 방법이거든.

useState — 로컬 기억

모든 입력 박스, toggle, loading flag가 useState에 살아. 값이 바뀌면 React가 컴포넌트와 그 자손을 다시 그려. setter 함수는 render 사이에도 같은 참조를 유지해. reference equality 걱정 없이 자식에게 내려도 돼.

useEffect — render 밖으로 뻗기

render 에 해야 하는 fetch, subscribe, scroll, document title 변경은 useEffect에 들어가. dependency array는 장식이 아니야. 빈 배열이면 mount 뒤 한 번 실행하고, 값이 들어 있으면 그중 하나가 바뀔 때 다시 실행해.

고전적 함정: inline 으로 정의된 함수 dependency 박지 마. render 마다 새 reference 야. useCallback 으로 먼저 감싸거나, 컴포넌트 밖으로 옮겨. (cwkPippa 초기에 아빠가 이거 잡았어 — chat list 가 keystroke 마다 re-render 됐는데, effect dep 가 매번 fresh 한 arrow function 가리키고 있었거든.)

useRef — escape hatch

실제 DOM 노드 가리켜야 할 때 (input focus, list scroll, non-React 라이브러리 통합), useRef 가 stable 한 컨테이너 줘. .current는 바꿀 수 있지만 바꾼다고 re-render가 일어나진 않아. 그게 핵심이야. 화면 계약에 속하지 않는 값을 담아.

셋을 상태의 종류로 나누면 덜 헷갈려. 화면에 보여서 바뀌면 다시 그려야 하는 값은 state, render 뒤 외부 세계와 맞춰야 하는 일은 effect, render와 무관하게 같은 손잡이를 붙들어야 하는 값은 ref야. ref에 화면 값을 숨기거나 effect에서 계산 가능한 state를 또 만들면 React가 아니라 두 번째 상태 기계를 만든 셈이야. 영리해 보이지만 디버깅할 때 바로 이자를 받아.

Code

useChat — chat view 전체를 움직이는 hook·tsx
function useChat(conversationId: string) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);
  const abortRef = useRef<AbortController | null>(null);

  // Load on mount + whenever conversation changes
  useEffect(() => {
    let cancelled = false;
    fetch(`/api/conversations/${conversationId}`)
      .then(r => r.json())
      .then(data => { if (!cancelled) setMessages(data.messages); });
    return () => { cancelled = true; };
  }, [conversationId]);

  const sendMessage = useCallback(async (text: string) => {
    abortRef.current?.abort();
    abortRef.current = new AbortController();
    setIsStreaming(true);
    try {
      await streamChat(conversationId, text, abortRef.current.signal, chunk => {
        setMessages(prev => applyChunk(prev, chunk));
      });
    } finally {
      setIsStreaming(false);
    }
  }, [conversationId]);

  return { messages, isStreaming, sendMessage, stop: () => abortRef.current?.abort() };
}

Progress

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

댓글 0

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

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