useState, useEffect, useRef

App Router가 React의 흐름이 되면서

Navigation, Form, UI 관련해서 새로운 Hook들이 추가되었다.

그전에 React에서 많이 쓰이는 Hook들을 정리해보려고 한다.

useState, useEffect, useRef부터 자주 쓰이는 Hook들을 확인하고,

사용 중 실수할 수 있는 부분도 함께 정리해보려고 한다.


useState – 상태 값을 저장할 때

사용자 입력, 버튼 클릭, 토글처럼 변하는 데이터를 화면에 반영할 때 가장 자주 쓰이는 Hook이다.


import { useState } from "react";

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

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

useState는 [값, 값을 업데이트하는 함수] 형태의 배열을 반환한다.

상태가 바뀌면 컴포넌트는 다시 렌더링된다.

상태 업데이트 방식에서 실수하기 쉬운 부분

setCount(count + 1);
setCount(count + 1);

이렇게 작성하면 두 번 더해질 것 같지만 실제로는 한 번만 반영된다.

React의 상태 업데이트는 비동기로 처리되기 때문에, 위 코드는 이전 값 기준이 아니라 현재 렌더링 시점의 값만 참조하기 때문이다.

setCount(prev => prev + 1);
setCount(prev => prev + 1); // 이건 실제로 +2가 된다

useEffect – 렌더링 이후 무언가를 처리할 때

API 요청, 타이머 설정, 이벤트 리스너 등록 등

화면이 그려진 뒤에 처리해야 하는 작업들을 수행할 때 사용한다.

import { useEffect, useState } from "react";

export default function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <p>{seconds}초가 지났습니다.</p>;
}

두 번째 인자인 의존성 배열([])에 어떤 값이 들어가느냐에 따라 실행 조건이 달라진다.

의존성 배열실행 시점
없음매번 렌더링될 때마다 실행됨
[]마운트 시 한 번만 실행됨
[count]count가 변경될 때마다 실행됨

cleanup 함수를 리턴하지 않으면, 타이머나 이벤트 리스너가 쌓여서 메모리 누수가 발생할 수 있다.


useRef – 값은 유지하지만 렌더링에는 영향을 주지 않을 때

import { useRef } from "react";

export default function FocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleClick = () => {
    inputRef.current?.focus();
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>포커스 이동</button>
    </>
  );
}

useRef는 .current를 통해 DOM에 직접 접근할 수 있다.

또한 컴포넌트가 다시 렌더링되더라도 값을 유지할 수 있기 때문에,

렌더링을 트리거하지 않고 데이터를 저장해두는 용도로도 자주 사용된다.


이전 값을 저장하고 싶을 때

상태가 바뀔 때마다 이전 값과 비교해야 하는 경우가 있다.

예를 들어 숫자가 증가 중인지, 감소 중인지를 판단하는 상황에서는 useRef가 유용하게 쓰인다.

import { useEffect, useRef, useState } from "react";

export default function TrendingNumber() {
  const [count, setCount] = useState(0);
  const prevCount = useRef(count);

  const isIncreasing = count > prevCount.current;

  useEffect(() => {
    prevCount.current = count;
  }, [count]);

  return (
    <div>
      <p>현재 값: {count}</p>
      <p>방향: {isIncreasing ? "증가 중" : "감소 중"}</p>
      <button onClick={() => setCount(prev => prev + 1)}>+1</button>
      <button onClick={() => setCount(prev => prev - 1)}>-1</button>
    </div>
  );
}

버튼을 눌러 상태가 바뀌면 컴포넌트는 다시 렌더링된다.

이때 prevCount.current는 이전 렌더링에서의 count 값을 그대로 가지고 있다.

예를 들어 숫자가 2일 때 +1을 누르면, • 현재 값은 3 • 이전 값은 2 → 그래서 “증가 중”으로 판단된다.

다시 -1을 눌러서 3 → 2로 내려가면 • 현재 값은 2 • 이전 값은 3 → 이때는 “감소 중”이 된다.

이런 식으로 방금 전 상태와 현재 상태를 비교해야 하는 상황에서

useRef는 간단하면서도 유용한 도구가 된다.


정리

Hook언제 쓰나특징
useState변하는 값을 관리할 때상태가 바뀌면 리렌더링됨
useEffect외부 작업(API, 타이머 등) 처리 시렌더링 이후 실행됨
useRef렌더링 없이 값 저장 or DOM 접근값은 유지되지만 렌더링되지 않음

이제 다음 파트에서는

useCallback, useMemo, useContext, useReducer, useLayoutEffect를 이어서 정리할 예정이다.