TestForge | Aidevops | 📊 Plogger ✍️ Blog 📚 Docs
plogger

AI DevOps Korea

AI 서비스 개발, 운영, 성능개선을 하나의 루프로 연결합니다

aidevops.kr에서 LLMOps, RAG, AI Agent, 관측성, 평가, 비용-성능 최적화를 실전 운영 관점으로 정리합니다.

React Hooks 설계 가이드

· 수정 4월 16일
React Hooks 설계 가이드 다이어그램
이 글에서 다루는 핵심 흐름, 아키텍처 구조, 주요 판단 포인트를 한눈에 이해할 수 있도록 정리한 그림입니다.
React Hooks는 클래스 컴포넌트를 대체하는 문법이 아니라, UI 상태와 부수효과를 함수 단위로 분리하게 만드는 모델입니다. 입문 단계에서는 `useState`와 `useEffect` 사용법만 익혀도 되지만, 실무로 갈수록 더 중요한 것은 "어떤 상태를 어디에 둘 것인가", "어떤 effect가 정말 필요한가", "반복되는 로직을 어떤 단위로 추출할 것인가"입니다.

좋은 Hook 설계는 컴포넌트를 짧게 만드는 것보다, 상태 흐름과 렌더링 비용을 읽기 쉽게 만드는 데 목적이 있습니다.

useState는 가장 가까운 곳에 둔다

지역 상태는 가능한 한 상태를 실제로 사용하는 컴포넌트 가까이에 두는 것이 좋습니다. 너무 위로 끌어올리면 불필요한 리렌더링과 props 전달이 늘고, 너무 아래로 내리면 상태 동기화가 어려워집니다.

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount((prev) => prev + 1)}>+1</button>
    </div>
  )
}

setState에서 이전 값을 기반으로 갱신할 때 함수형 업데이트를 쓰는 습관은 중요합니다. 비동기 이벤트나 연속 업데이트에서 상태 경합을 줄일 수 있기 때문입니다.

useEffect는 “동기화”가 필요할 때만 쓴다

많은 React 코드가 어려워지는 이유는 useEffect를 너무 쉽게 사용하기 때문입니다. Effect는 렌더링 이후 외부 세계와 상태를 동기화할 때 쓰는 도구입니다. 서버 요청, DOM API 연동, 이벤트 구독, 타이머, 외부 라이브러리 연결 같은 경우가 대표적입니다.

반대로 파생 가능한 값 계산이나 이벤트에 대한 직접 응답은 effect 없이 처리하는 편이 더 단순합니다.

import { useEffect, useState } from 'react'

function UserProfile({ userId }) {
  const [user, setUser] = useState(null)

  useEffect(() => {
    let cancelled = false

    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then((data) => {
        if (!cancelled) setUser(data)
      })

    return () => {
      cancelled = true
    }
  }, [userId])

  return <div>{user?.name}</div>
}

실무에서는 effect가 많아질수록 다음 질문을 자주 해야 합니다.

  • 이 값은 그냥 렌더 중 계산할 수 없는가
  • 이 로직은 사용자 이벤트 핸들러 안에서 끝낼 수 없는가
  • 이 effect는 하나의 책임만 갖고 있는가
  • 의존성 배열을 숨기기 위해 eslint를 끄고 있지는 않은가

useRef는 렌더링과 무관한 값을 보관할 때 유용하다

useRef는 DOM 접근뿐 아니라, 렌더를 유발하지 않아야 하는 값을 유지할 때도 좋습니다. 타이머 ID, 이전 값, 외부 인스턴스 핸들, 중복 실행 방지 플래그 등이 대표적입니다.

import { useRef } from 'react'

function TextInput() {
  const inputRef = useRef(null)

  function focusInput() {
    inputRef.current?.focus()
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>포커스</button>
    </>
  )
}

상태처럼 화면에 반영되어야 하는 값을 useRef에 넣으면 UI와 데이터가 어긋날 수 있습니다. 반대로 화면에 직접 영향을 주지 않는 값을 useState에 넣으면 불필요한 리렌더링이 늘어납니다.

메모이제이션은 기본값이 아니라 최적화 도구다

useMemo, useCallback은 성능 최적화가 실제로 필요할 때만 쓰는 것이 좋습니다. 모든 계산과 함수를 메모이제이션하면 오히려 코드가 읽기 어려워지고, 의존성 관리도 복잡해집니다.

중요한 기준은 두 가지입니다.

  • 계산 비용이 큰가
  • 참조 안정성이 실제로 하위 컴포넌트 최적화에 영향을 주는가

최적화는 프로파일링이나 구체적인 렌더링 병목을 보고 도입하는 편이 낫습니다.

Custom Hook은 반복 로직이 아니라 경계를 추출한다

좋은 커스텀 Hook은 단순히 코드를 다른 파일로 옮긴 것이 아닙니다. 특정 책임을 갖는 상태-이벤트-효과 묶음을 재사용 가능한 단위로 만든 것입니다.

import { useEffect, useState } from 'react'

export function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    let cancelled = false

    setLoading(true)
    setError(null)

    fetch(url)
      .then((res) => res.json())
      .then((json) => {
        if (!cancelled) {
          setData(json)
          setLoading(false)
        }
      })
      .catch((err) => {
        if (!cancelled) {
          setError(err)
          setLoading(false)
        }
      })

    return () => {
      cancelled = true
    }
  }, [url])

  return { data, loading, error }
}

이런 Hook은 데이터 요청이라는 경계를 추출한 것입니다. 다만 서버 상태 관리가 본격화되면 직접 Hook을 쌓기보다 TanStack Query 같은 도구를 쓰는 편이 재시도, 캐싱, stale 정책에서 더 강력합니다.

Hooks를 잘못 쓰면 생기는 문제

가장 흔한 문제는 effect 안에서 상태를 다시 세팅하며 무한 루프를 만드는 것입니다. 그 다음은 하나의 effect에 데이터 요청, 이벤트 바인딩, 파생 상태 계산을 전부 몰아 넣는 경우입니다.

또 다른 문제는 모든 공통 로직을 Custom Hook으로 추출하려는 습관입니다. 재사용보다 가독성이 더 중요한 경우도 많습니다. 아직 책임이 명확하지 않은 로직은 컴포넌트 안에 두는 편이 낫습니다.

실무에서 기억할 기준

React Hooks를 잘 쓰는 팀은 API 이름보다 경계를 잘 나눕니다.

  • 로컬 UI 상태와 서버 상태를 구분한다
  • effect는 외부 동기화에만 사용한다
  • 커스텀 Hook은 책임 단위로 추출한다
  • 메모이제이션은 증거가 있을 때만 쓴다
  • 상태는 가장 가까운 소유자에게 둔다

마무리

React Hooks는 문법을 많이 아는 것보다 상태와 부수효과를 올바르게 배치하는 감각이 중요합니다. useState, useEffect, useRef 각각의 역할을 분명히 이해하면 컴포넌트는 더 짧아질 뿐 아니라, 더 읽기 쉽고 수정하기 쉬운 구조가 됩니다. Hook은 기능이 아니라 설계 도구라는 관점으로 접근할 때 가장 큰 효과를 냅니다.

운영 환경에서 어려워지는 지점

  • 훅은 effect 시점, stale closure, 암묵적 의존성을 아키텍처 문제로 다루지 않으면 빠르게 불안정해진다.
  • 커스텀 훅은 너무 많은 동작을 숨기면 오히려 로컬 로직보다 디버깅이 어려워질 수 있다.
  • 큰 앱에서는 훅의 개수보다 훅 인터페이스 설계가 더 자주 문제를 만든다.

중요한 아키텍처 결정

  • 훅은 단순히 코드를 옮기는 수단이 아니라 재사용 가능한 상태 기반 동작 단위를 캡슐화할 때 만든다.
  • effect는 일반 제어 흐름이 아니라 동기화 작업에 좁게 사용한다.
  • 커스텀 훅은 입력과 출력이 안정적이도록 설계해 소비 코드 테스트가 쉬워야 한다.

실무 예시

좋은 훅 경계는 하나의 책임을 분명하게 설명한다.

function useDebouncedSearch(query: string, delay = 300) {
  const [debouncedQuery, setDebouncedQuery] = useState(query)

  useEffect(() => {
    const timer = window.setTimeout(() => setDebouncedQuery(query), delay)
    return () => window.clearTimeout(timer)
  }, [query, delay])

  return debouncedQuery
}

피해야 할 안티패턴

  • 한 화면에서 일어난다는 이유로 API 호출, analytics, DOM 변경, navigation을 한 훅에 넣는 것.
  • effect 설계를 고치지 않고 dependency 경고를 억누르는 것.
  • 명확한 계약 없이 큰 상태 묶음과 callback 집합을 반환하는 훅을 만드는 것.

운영 체크리스트

  • effect 의존성은 lint 통과가 아니라 동기화 의도 기준으로 리뷰한다.
  • 커스텀 훅은 실패와 타이밍 시나리오까지 테스트한다.
  • 훅 이름은 구현 세부가 아니라 책임에 맞춘다.
  • 한 번만 쓰이고 재사용 가치가 없는 훅이 불필요한 간접화인지 점검한다.

최종 판단

훅은 명시적 의존성을 가진 하나의 응집된 동작을 캡슐화할 때 잘 확장된다. 지저분한 컴포넌트 로직의 은신처가 되면 오히려 더 나빠진다.

Continue Reading

다음으로 읽기 좋은 글

다음 탐색

이 주제를 시스템 관점으로 더 이어서 보기