모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

14강 / 전체 30강

컴포넌트 합성과 재사용

5분 읽기 조회 4

useRef로 DOM 요소에 직접 접근하고, 렌더링 없이 값을 유지하는 방법, forwardRef 패턴까지 useRef의 두 가지 역할을 완전히 이해합니다.

🔍 useRef — 두 가지 역할

useRef는 용도가 두 가지입니다. 혼동하기 쉬우니 처음부터 명확히 구분합니다:

  1. DOM 요소에 직접 접근: 특정 DOM 요소를 직접 제어할 때 (포커스, 스크롤, 크기 측정 등)
  2. 렌더링 없이 값 유지: 리렌더링을 일으키지 않고 값을 기억해야 할 때 (이전 값 저장, 타이머 ID 등)

📍 역할 1 — DOM 요소에 접근하기

입력창 자동 포커스, 스크롤 위치 제어 같은 작업은 DOM에 직접 접근해야 합니다. useRef로 요소에 연결합니다:

import { useRef, useEffect } from 'react'

function SearchModal({ isOpen }: { isOpen: boolean }) {
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    // 모달이 열릴 때 자동으로 입력창에 포커스
    if (isOpen && inputRef.current) {
      inputRef.current.focus()
    }
  }, [isOpen])

  return (
    <div>
      {/* ref 속성으로 DOM 요소와 연결 */}
      <input
        ref={inputRef}
        type="search"
        placeholder="검색..."
        className="border rounded px-4 py-2 w-full"
      />
    </div>
  )
}

inputRef.current가 실제 DOM 요소입니다. useEffect 없이 렌더링 중에 inputRef.current를 읽으면 null입니다 — DOM은 렌더링 후에 연결됩니다.

💾 역할 2 — 렌더링 없이 값 유지

useState는 값이 바뀌면 리렌더링을 일으킵니다. 그런데 때로는 값은 기억하되, 리렌더링은 일으키고 싶지 않을 때가 있습니다. 타이머 ID가 대표적입니다:

function Stopwatch() {
  const [time, setTime] = useState(0)
  const [isRunning, setIsRunning] = useState(false)
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)

  function start() {
    if (isRunning) return
    setIsRunning(true)
    intervalRef.current = setInterval(() => {
      setTime((prev) => prev + 1)
    }, 1000)
  }

  function stop() {
    if (intervalRef.current !== null) {
      clearInterval(intervalRef.current)
      intervalRef.current = null
    }
    setIsRunning(false)
  }

  function reset() {
    stop()
    setTime(0)
  }

  return (
    <div className="text-center space-y-4">
      <p className="text-4xl font-mono font-bold">{time}초</p>
      <div className="flex gap-2 justify-center">
        {!isRunning ? (
          <button onClick={start} className="px-4 py-2 bg-green-500 text-white rounded">시작</button>
        ) : (
          <button onClick={stop} className="px-4 py-2 bg-red-500 text-white rounded">정지</button>
        )}
        <button onClick={reset} className="px-4 py-2 bg-gray-200 rounded">초기화</button>
      </div>
    </div>
  )
}

intervalRef를 useState로 관리하면 타이머 ID가 바뀔 때마다 불필요한 리렌더링이 발생합니다. useRef로 관리하면 값은 유지되지만 리렌더링 없이 조용히 저장됩니다.

⏪ 이전 값 기억하기

useRef로 이전 렌더의 값을 기억하는 유용한 패턴입니다:

function PriceDisplay({ price }: { price: number }) {
  const prevPriceRef = useRef<number>(price)

  useEffect(() => {
    prevPriceRef.current = price  // 렌더 후 이전 값 업데이트
  })

  const prevPrice = prevPriceRef.current
  const change = price - prevPrice

  return (
    <div>
      <span className="text-2xl font-bold">{price.toLocaleString()}원</span>
      {change !== 0 && (
        <span className={change > 0 ? 'text-red-500' : 'text-blue-500'}>
          {change > 0 ? '▲' : '▼'} {Math.abs(change).toLocaleString()}
        </span>
      )}
    </div>
  )
}

🔗 forwardRef — ref를 자식 컴포넌트로 전달

직접 만든 컴포넌트에는 ref를 기본으로 전달할 수 없습니다. forwardRef로 감싸야 합니다:

import { forwardRef } from 'react'

interface InputProps {
  label: string
  placeholder?: string
}

// forwardRef로 ref를 받아서 실제 input 요소로 전달
const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ label, placeholder }, ref) => {
  return (
    <label className="block">
      <span className="text-sm font-medium text-gray-700">{label}</span>
      <input
        ref={ref}  // 전달받은 ref를 DOM에 연결
        placeholder={placeholder}
        className="mt-1 block w-full border rounded px-3 py-2"
      />
    </label>
  )
})

CustomInput.displayName = 'CustomInput'  // DevTools 표시명

// 사용
function ParentForm() {
  const nameInputRef = useRef<HTMLInputElement>(null)

  function focusName() {
    nameInputRef.current?.focus()
  }

  return (
    <div>
      <CustomInput ref={nameInputRef} label="이름" />
      <button onClick={focusName}>이름 입력창 포커스</button>
    </div>
  )
}

✅ 정리

  • useRef는 ① DOM 요소 접근, ② 렌더링 없이 값 유지 두 가지 역할입니다.
  • ref.current로 값에 접근합니다. DOM ref는 렌더링 후에야 연결됩니다.
  • 타이머 ID, 이전 값 등 리렌더링 없이 유지할 값에 사용합니다.
  • 직접 만든 컴포넌트에 ref를 전달하려면 forwardRef로 감쌉니다.

관련 주제

  • composition 패턴
  • children 슬롯
  • 재사용 가능한 컴포넌트 설계
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 리액트 입문 — 컴포넌트로 만드는 웹
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

리액트 입문 — 컴포넌트로 만드는 웹

이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...