강의

멘토링

커뮤니티

인프런 커뮤니티 질문&답변

관태님의 프로필 이미지
관태

작성한 질문수

React 마스터 클래스: Part 2 - 미션으로 완성하는 고성능 훅과 실전 아키텍처

🚩 [미션 18: 중급] 실시간 환율 변동 추적기 (Price Diff)

useRef를 활용한 이전 상태 추적 시 발생하는 ESLint 에러(react-hooks/refs)에 대해 질문드립니다.

해결된 질문

작성

·

28

·

수정됨

0

안녕하세요 강사님!
수업 내용을 바탕으로 useRef를 이용해 직전 상태값을 저장하고 관리하는 로직을 실습하던 중 궁금한 점이 생겨 질문드립니다.

강의 내용처럼 useRef를 사용하여 렌더링 사이의 값을 보관하고 이를 화면에 출력하려고 코드를 작성했습니다.

이때 최신 ESLint 규칙에서 "Cannot access refs during render"라는 에러(react-hooks/refs)가 발생합니다.

제가 공부한 바로는 리액트의 '렌더 단계(Render Phase)'의 순수성을 지키고 데이터 불일치를 방지하기 위해 렌더링 도중 Ref 접근을 금지한다고 이해했습니다.

이와 관련하여 강사님에게 질문드립니다.

만약 ref를 사용하는 현재의 구조를 유지하면서 리액트의 렌더링 원칙을 준수할 수 있는 더 나은 패턴(예: 커스텀 훅 등)이 있다면 무엇인지 조언 부탁드립니다.

감사합니다~!

답변 1

1

nhcodingstudio님의 프로필 이미지
nhcodingstudio
지식공유자

안녕하세요 관태님! 질문 주셔서 감사합니다.

관태님께서 마주하신 react-hooks/refs 에러인 "Cannot access refs during render"는 React 18 이후 동시성 모드(Concurrent Mode)가 도입되면서 더욱 엄격해진 규칙인데, 이는 렌더링 단계(Render Phase)가 사진을 찍는 순간처럼 순수해야 하기 때문입니다. React의 렌더링 과정은 크게 컴포넌트 함수를 호출하고 JSX를 만들어내는 계산 단계인 렌더 단계와 실제 DOM에 변경 사항을 반영하고 useEffect 등을 실행하는 적용 단계인 커밋 단계로 나뉘는데, 여기서 중요한 점은 렌더 단계는 언제든 멈추거나, 취소되거나, 여러 번 재실행될 수 있다는 점입니다.

그런데 useRef는 값이 바뀌어도 렌더링을 유발하지 않는 가변적인 저장소이기 때문에 만약 렌더링 도중에 ref.current 값을 읽거나 쓰게 되면 React가 렌더링을 잠깐 멈춘 사이 값이 바뀌어버려 같은 렌더링 사이클 안에서도 화면의 위쪽은 값 A를 보여주고 아래쪽은 값 B를 보여주는 '티어링(Tearing)' 현상이 발생할 수 있어 ESLint가 이를 엄격하게 경고하는 것입니다.

실무에서는 이 문제를 해결하고 재사용성을 높이기 위해 usePrevious라는 커스텀 훅을 만들어 사용하는 것이 표준이며, 이 훅의 핵심은 Ref의 업데이트 시점을 렌더링 도중이 아닌 렌더링이 다 끝난 후인 이펙트 단계로 미루는 것입니다. 기존 코드에서 직접 ref.current를 읽는 부분을 커스텀 훅으로 분리하면 다음과 같이 깔끔하고 안전해집니다.

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

// [실무 패턴] 별도의 유틸리티 파일(hooks/usePrevious.js)로 분리해서 쓰는 것을 추천합니다.
function usePrevious(value) {
  const ref = useRef();

  // 1. 렌더링이 다~ 끝나고 화면이 그려진 뒤(Commit Phase)에 값을 저장합니다.
  useEffect(() => {
    ref.current = value;
  }, [value]);

  // 2. 렌더링 도중에는 아직 useEffect가 실행되기 전이므로
  //    ref에는 '이전 렌더링'에서 저장해둔 값이 들어있습니다.
  return ref.current;
}

export default function ExchangeRateTracker() {
  const [rate, setRate] = useState(1300);
  
  // 커스텀 훅을 사용해 '이전 값'을 안전하게 가져옵니다.
  // 내부적으로 useEffect를 쓰기 때문에 렌더링 흐름을 방해하지 않습니다.
  const prevRate = usePrevious(rate);
  const isFirstRender = useRef(true);

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }
    // 환율 변동 알림
    alert(`환율이 ${rate}원으로 변동되었습니다.`);
  }, [rate]);

  return (
    <div style={{ padding: "20px", border: "1px solid #ddd", borderRadius: "10px" }}>
      <h3>💹 실시간 환율 추적기</h3>
      <p>현재 환율: <b>{rate}원</b></p>
      {/* 이제 prevRate를 일반 변수처럼 편하게 사용하면 됩니다 */}
      <p>직전 환율: <b>{prevRate !== undefined ? `${prevRate}원` : "데이터 수집 중..."}</b></p>

      <div style={{ fontSize: "24px", margin: "10px 0" }}>
        변동: {
          prevRate === undefined ? "-" :
          rate > prevRate ? "▲ 상승" :
          rate < prevRate ? "▼ 하락" : "-"
        }
      </div>

      <button onClick={() => setRate(prev => prev + 10)}>환율 올리기 (+10)</button>
      <button onClick={() => setRate(prev => prev - 10)}>환율 내리기 (-10)</button>
    </div>
  );
}

이 패턴이 안전한 이유는 작동하는 타이밍 덕분인데, 예를 들어 첫 번째 렌더링 시 rate가 1300이라면 usePreviousundefined를 반환하고 화면이 그려진 후 useEffect가 실행되어 ref.current에 1300을 저장하게 되며, 이후 rate가 1310으로 바뀌어 두 번째 렌더링이 일어날 때 usePrevious를 호출하면 아직 두 번째 useEffect가 실행되기 전이므로 ref.current에는 아까 저장해둔 1300이 들어있게 되어 이것이 바로 prevRate가 되는 것입니다.

추가로 여기서 한 가지 더 중요한 점은 왜 굳이 useState가 아닌 useRef를 쓰는지에 대한 성능적인 이유입니다. 만약 이전 값을 저장하기 위해 useState를 사용한다면 useEffect 내부에서 setPrevState를 호출하는 순간 리액트는 상태가 바뀌었다고 판단하여 불필요한 재렌더링을 한 번 더 유발하게 되지만, 반면 useRef는 값이 바뀌어도 렌더링을 유발하지 않기 때문에 성능 저하 없이 조용히 데이터를 기록하는 '그림자 저장소' 역할을 수행하기에 가장 적합한 도구입니다.

실무에서는 매번 usePrevious를 직접 구현하기보다 잘 관리되는 훅 라이브러리를 사용하는 경우가 많은데, 대표적으로 가장 방대한 훅 모음집인 react-use나 알리바바에서 만든 고품질 훅 라이브러리인 ahooks가 업계 표준에 가깝습니다.

결론적으로 관태님의 코드가 틀린 것은 아니지만 React의 렌더링 순수성을 지키고 ESLint 경고를 해결하면서 성능까지 챙기기 위해서는 usePrevious 커스텀 훅 패턴으로 로직을 분리하는 것이 가장 우아한 해결책이며, 이 개념까지 이해하신다면 React의 상태 관리를 훨씬 더 효율적으로 하실 수 있을 겁니다.

참고해주세요. 감사합니다!

관태님의 프로필 이미지
관태

작성한 질문수

질문하기