8.1 useRef란?8.1.1 useRef의 사용 목적8.2 useRef vs useState vs 일반변수8.2.1 useRef와 useState의 차이8.2.2 useRef 사용해보기8.3 useRef와 DOM8.3.1 useRef로 특정한 DOM 선택8.3.2 useRef로 DOM 선택 후 사용 시 주의 사항8.4 (실습) useRef 응용해보기8.4.1 useRef를 사용하여 focus 관리하기
8.1 useRef란?
const ref = useRef(initialValue);
useRef의 기본 형태입니다. 함수형 컴포넌트에서 useRef는 순수 자바스크립트 객체를 생성합니다. 생성된 객체는
{ current: initialValue }
의 형태로 반환이 되기 때문에 ref.current
의 형태로 값의 접근이 가능합니다. 따라서 useRef는 .current
프로퍼티에 변경 가능한 값을 담고 있는 “상자”와 같다고 표현할 수 있습니다.8.1.1 useRef의 사용 목적
useRef는 크게 2가지의 경우에 사용이 됩니다.
- 특정 DOM에 접근하여 제어해야 할 경우
- 렌더링과 관계없이 값을 변경하고 싶을 경우
자바스크립트의 경우, getElementById, querySelector 등의 메서드를 통해 쉽게 DOM에 접근하여 제어할 수 있습니다. 하지만 React의 경우에는 직접 DOM을 조작하는 것을 권장하고 있지 않습니다. 그럼에도 불구하고 스크롤바 위치를 파악해야 할 경우나 input 요소에 자동 포커스를 설정해 주어야 할 경우처럼 직접 DOM을 제어해 주어야 하는 예외적인 상황이 발생할 수 있는데, 이때 useRef를 사용해 DOM에 접근할 수 있습니다.
React에서 DOM의 직접 조작을 권장하지 않는 이유
React는 Virtual DOM을 사용하기 때문에 DOM 조작으로 인한 브라우저 렌더링을 최소화한다는 장점을 가지고 있습니다. 때문에 직접 DOM을 조작하게 된다면 “Virtual DOM을 사용한 브라우저 렌더링 최소화” 라는 React의 장점을 놓치는 일이 발생하게 됩니다. 따라서 가능하다면 직접 DOM을 제어하는 것을 지양하는 것이 좋습니다.
또한 useRef를 사용하여 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지됩니다. 컴포넌트의 생애주기란 DOM에 마운트되고 언마운트되기까지의 과정을 말합니다. useRef는
.current
의 값이 변경되어도 컴포넌트 리렌더링을 발생시키지 않고, 렌더링을 할 때 동일한 ref 객체를 제공하게 됩니다. 즉, 컴포넌트가 계속해서 렌더링 되더라도 컴포넌트가 언마운트되기 전까지 값이 그대로 유지됩니다. 따라서 컴포넌트 값의 변경은 관리해야 하지만 리렌더링을 발생시킬 필요는 없을 때 활용할 수 있습니다.8.2 useRef vs useState vs 일반변수
8.2.1 useRef와 useState의 차이
useState 와 useRef 는 모두 상태 관리를 위해 사용할 수 있습니다. 다만 useState 의 경우 state 가 변경된 후에 리렌더링을 진행하는 반면, useRef 는 리렌더링을 진행하지 않습니다. useRef 는 내부적으로 값이 변하여 컴포넌트가 렌더링 되기 전까지는 변경된 값이 보이지 않다가 렌더링이 되는 시점에 변경된 값이 보이게 됩니다. 이러한 특성에 맞추어 렌더링이 필요한 state 의 경우에는 useState 를 사용하는 용도와 생애 주기 내내 변화하는 값을 가리키고 있다는 차별점을 가지고 있습니다.
즉, useState 는 컴포넌트의 생애 주기와 밀접한 연관이 있는 요소이므로 렌더링과 관련이 없는 값을 저장하기에는 적합하지 않으며, useRef 는 state 렌더링과 무관한 값과 이전의 값을 저장하기에 적합하다 할 수 있습니다. 따라서 각각의 상황에 맞게 Hook을 잘 사용해야 되겠습니다.
8.2.2 useRef 사용해보기
아래 예제는 각 버튼을 누를 때마다 +1 씩 카운트가 되는 코드입니다. 어떤 이유로 useRef 를 사용해야 하는지 예제를 통해 함께 살펴보겠습니다.
import { useRef, useState } from "react"; import "./styles.css"; export default function App() { const [stateCount, setStateCount] = useState(0); const refCount = useRef(0); return ( <div className="App"> <button onClick={() => { setStateCount((prev) => prev + 1); }} > State 버튼 </button> <button onClick={() => { refCount.current += 1; }} > Ref 버튼 </button> <br /> <br /> <div>useState Count: {stateCount}</div> <div>useRef Count: {refCount.current}</div> </div> ); }
State 버튼
을 누르면 useState Count 값이 1이 된 것을 확인할 수 있지만, Ref 버튼
을 누르면 useRef Count 는 변경된 값이 렌더링 되지 않고 있습니다. 정말로 useRef Count 값은 카운트가 되지 않는 것일까요? 그렇지 않습니다. 내부적으로는 카운트가 올라가고 있지만 화면에 바로 렌더링이 되지 않을 뿐입니다. 다시 한 번 State 버튼
을 눌러 리렌더링을 시키게 되면 useRef Count 값도 올라가게 됩니다. state 가 변화됐을 때, refCount.current
의 값도 마찬가지로 화면에 렌더링 되는 것임을 알 수 있습니다. 이유는 useRef 로 부터 생성된 객체는 useRef 를 통해 반환된 객체는 component 의 생애주기 내내 변화하는 값을 가리키고 있기 때문에 current 값이 변화해도 리렌더링에 관여하지 않는다는 것에 있다고 볼 수 있습니다.8.3 useRef와 DOM
앞서 살펴본 useRef를 사용하는 이유 중에는 특정 DOM에 접근하여 제어해야 할 경우가 있었습니다.
React에서 DOM을 선택하기 위해서는
ref
를 사용하면 되는데, ref를 사용하기 위해서는 useRef라는 React의 Hook을 사용합니다. 간단한 코드를 통해 DOM을 선택하는 방법을 알아봅시다.8.3.1 useRef로 특정한 DOM 선택
import React, { useRef } from "react"; function App() { const textInput = useRef(null); // --- ⓵ useRef 생성 const ClickBtn = () => { console.log("Click!"); }; const handleClickBtn = () => { textInput.current.focus(); // --- ⓷ useRef가 가리키는 input 태그에 포커스 이벤트 적용 }; return ( <div> <input type="text" /> <input type="button" value="ref X" onClick={ClickBtn} /> <br /> <input type="text" ref={textInput} /> // --- ⓶ ref props로 전달 <input type="button" value="ref O" onClick={handleClickBtn} /> </div> ); }; export default App;
위 코드는 useRef를 사용하여 input의 값을 가져온 예제와 그렇지 않은 예제를 비교하기 위한 코드입니다.

먼저, 첫 번째 input은 onClick 이벤트가 발생했을 경우 콘솔만 찍힐 뿐 다른 변화는 생기지 않습니다. 하지만 두 번째 input은 onClick 이벤트가 발생했을 때 input 태그에 포커스가 적용되는데 다음과 같은 흐름으로 적용이 됩니다.
import React, { useRef } from "react"; const textInput = useRef(null); // --- ⓵ useRef 생성 <input type="text" ref={textInput} /> // --- ⓶ ref props로 전달 <input type="button" value="ref O" onClick={handleClickBtn} />
useRef를 생성하기 위해 react 모듈에서 useRef를 import 해온 후, text input에 대한 useRef를 생성합니다. 그리고 값을 받아오고 싶은 input 태그에 ref props로 전달해줍니다.
const handleClickBtn = () => { textInput.current.focus(); // --- ⓷ useRef가 가리키는 input 태그에 포커스 이벤트 적용 };
버튼을 클릭했을 때, handleClickBtn 함수가 실행되고
textInput.current.focus
를 통해 input 태그에 포커스를 적용합니다.
그러면 위와 같은 결과로 버튼을 클릭했을 때 input 태그에 포커스가 되는 것을 확인할 수 있습니다.
8.3.2 useRef로 DOM 선택 후 사용 시 주의 사항
useRef를 이용하여 순수 자바스크립트 객체를 생성하고 선택하고자 하는 태그에 ref props를 전달하면, 그때부터는 해당 태그가 선택되어 다양하게 활용될 수 있음을 input 태그 예제를 통해 확인해보았습니다. 그런데 이 DOM을 언제 사용하느냐에 따라 원하는 값이 나오기도 하고
null
이나 undefined
값이 나오기도 합니다. 코드를 통해 정확히 언제 그런 상황이 발생하는지, 원하는 바와 다른 결괏값이 나오지 않게 하기 위해서는 어떻게 사용해야 할지 알아보겠습니다. import React, { useRef } from "react"; import music from "./assets/music.mp3"; function Player() { const audioRef = useRef(null); audioRef.current.play(); console.log(audioRef.current); const handlePlay = () => { audioRef.current.play(); console.log("🎵재생🎵"); }; const handlePause = () => { audioRef.current.pause(); console.log("⏸중지!"); }; return ( <> <audio src={music} ref={audioRef} controls></audio> <br /> <div style={{ margin: "10px 89px" }}> <button onClick={handlePlay}> 🎵 재생 </button> <button onClick={handlePause}> ⏸ 중지 </button> </div> </> ); } export default Player;

위의 코드는 재생 버튼을 누르면 오디오가 실행되고 중지 버튼을 누르면 오디오가 중지되는 플레이어 예제입니다. 여기에 다음과 같이 audio에 대한 useRef를 생성하고 바로
ref.current
의 형태로 오디오에 접근해 봅시다.const audioRef = useRef(null); audioRef.current.play(); console.log(audioRef.current); ... return ( <audio src={music} ref={audioRef} controls></audio> );

값이 정의되지 않아 에러가 나고 있습니다. 왜 그런 걸까요? 여기서 우리는 React의 컴포넌트 렌더링 과정에 대해 이해하고 있어야 합니다.
React의 컴포넌트가 최초 렌더링 되었을 때를 일부 요약하여 정리하면 다음과 같은 순서를 따르게 됩니다.
- 함수 컴포넌트를 호출한다.
- props, state 등의 값을 초기화한다. (최초 마운트 시 1번만 실행)
- React DOM을 렌더링한다. (return 실행)
- 기존의 DOM에 반영한다.
플레이어 예제에서는 아직 React DOM을 렌더링하지 않았고, 기존 DOM에 반영되지 않은 채로
ref.current
의 형태로 참조를 하려고 하기 때문에 에러가 발생하게 됩니다.이러한 현상을 방지하기 위해서는 React DOM을 렌더링하고 기존의 DOM에 반영되고 난 후 사용하거나, 조건부 렌더링을 통해
ref.current
가 존재할 때에만 사용이 가능하도록 해야합니다.import React, { useEffect, useRef } from "react"; import music from "./assets/music.mp3"; function Player() { const audioRef = useRef(null); useEffect(() => { audioRef.current && audioRef.current.play(); // 유효성 검사 한 후 사용 }); const handlePlay = () => { audioRef.current.play(); console.log("🎵재생🎵"); }; const handlePause = () => { audioRef.current.pause(); console.log("⏸중지!"); }; return ( <> <audio src={music} ref={audioRef} controls></audio> <br /> <div style={{ margin: "10px 89px" }}> <button onClick={handlePlay}> 🎵 재생 </button> <button onClick={handlePause}> ⏸ 중지 </button> </div> </> ); } export default Player;
audio 요소의 controls 속성
재생, 정지, 볼륨 조절과 같은 오디오 기본적인 컨트롤러 표시 여부를 결정하는 속성입니다.
8.4 (실습) useRef 응용해보기
8.4.1 useRef를 사용하여 focus 관리하기
이번 파트에서는 useRef를 사용하여 특정 input 요소를 focus 할 수 있는 코드를 작성해보겠습니다.
아래 예제들은 회원가입 버튼을 누르면 입력하지 않은 input 요소로 바로 focus되어, 사용자가 누락된 input 요소 없이 작성할 수 있도록 돕습니다.
import React from "react"; function App() { return ( <form style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "10px", }} > <h2>프로필 설정</h2> <label> 이메일: <input type="email"></input> </label> <label> 비밀번호: <input type="password"></input> </label> <label> 비밀번호 확인: <input type="password"></input> </label> <label> 이름: <input type="text"></input> </label> <label> 닉네임: <input type="text"></input> </label> <button type="submit" style={{ width: "180px" }}> 회원가입하기 </button> </form> ); } export default App;
form에서 onsubmit 이벤트가 발생하면, input에서 입력한 값을 전달받습니다.
하지만 위의 코드만으로는 input에서 입력한 값을 전달받아올 수 없습니다. 따라서 아래 코드와 같이 useRef를 이용하여 input에서 입력한 값을 순수 자바스크립트 객체 형태로 받을 수 있도록 합니다. 여기서
inputRef
는 새로운 input 요소가 추가되어도 배열로써 처리가 가능하도록 기본값을 빈 배열 형태로 작성했습니다.import React, { useRef } from "react";
const inputRef = useRef([]);
React에서는 ref라는 프로퍼티를 통해 특정 DOM 요소를 참조하여 입력값을 받을 수 있습니다. ref를 이용하여 각 input 요소에서 입력한 값을 전달받아 아래의 코드처럼
inputRef
로 연결해줍니다.아래 예제들은 회원가입 버튼을 누르면 어떠한 값도 입력을 하지 않은 input 요소로 바로 focus되어, 누락된 input 요소 없이 사용자가 작성할 수 있도록 돕습니다.설정한 이름으로 변동되는 객체의 값을 받을 수 있습니다. 각 input 요소에서 변동되는 값을 받는 변수인
inputRef
를 연결시켜줍니다.여기서,
inputRef
는 각각의 input 요소로부터 전달받는 값을 구분하기 위해 ["email"]
나 ["pw"]
와 같이 괄호 표기법(bracket notation) 형태로 고유한 이름을 작성합니다.// --- ① 수정 전 코드 예시 <label> 이메일: <input type="email"></input> </label> <label> 비밀번호: <input type="password"></input> </label> <label> 비밀번호 확인: <input type="password"></input> </label> <label> 이름: <input type="text"></input> </label> <label> 닉네임: <input type="text"></input> </label> // --- ② 수정 후 코드 예시 <label> 이메일: <input type="email" ref={(el)=>(inputRef.current["email"]=el)}></input> </label> <label> 비밀번호: <input type="password" ref={(el)=>(inputRef.current["pw"]=el)}></input> </label> <label> 비밀번호 확인: <input type="password" ref={(el)=>(inputRef.current["pwcheck"]=el)} ></input> </label> <label> 이름:{" "} <input type="text" ref={(el)=>(inputRef.current["name"]=el)}></input> </label> <label> 닉네임: <input type="text" ref={(el)=>(inputRef.current["nickname"]=el)}></input> </label>
위의 코드를 모두 작성했다면, 아래 코드를 통해
inputRef
의 결과를 확인해보겠습니다.console.log(inputRef);
그림 8-5처럼
inputRef
안에는 current라는 연관배열을 두고 있습니다. 또한 위에서 입력한 괄호 표기법으로 input 값에 접근이 가능하다는 것을 확인할 수 있습니다.
여기서 연관배열이란 배열을 키(key)와 값(value)으로 나누어서 사용하는 것을 의미합니다.
마지막으로 제출양식에 따라 값을 입력하고서 회원가입하기 버튼을 클릭하여 제출했을 때, input 요소의 입력값이 누락되었다면, 해당 input 요소에 focus가 되는
inputCheck
함수를 추가합니다.const inputCheck = (e) => { e.preventDefault(); if (inputRef.current["email"].value === "") { console.log("이메일을 입력해주세요"); inputRef.current["email"].focus(); return; } else if (inputRef.current["pw"].value === "") { console.log("비밀번호를 입력해주세요"); inputRef.current["pw"].focus(); return; } else if (inputRef.current["pwcheck"].value === "") { console.log("비밀번호 확인을 입력해주세요"); inputRef.current["pwcheck"].focus(); return; } else if (inputRef.current["name"].value === "") { console.log("이름을 입력해주세요"); inputRef.current["name"].focus(); return; } else if (inputRef.current["nickname"].value === "") { console.log("닉네임을 입력해주세요"); inputRef.current["nickname"].focus(); return; } };
그리고 form에서 onsubmit 이벤트가 발생할 때,
inputCheck
가 실행되도록 아래의 코드로 수정합니다.<form style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "10px", }} onSubmit={inputCheck} >
그림 8-6은 이메일과 비밀번호 그리고 이름이 입력된 상태입니다. 하지만 비밀번호 확인과 닉네임은 작성하지 않은 상태이기 때문에 이때 회원가입하기 버튼을 누르면, 그림 8-7처럼 input 값이 빠진 비밀번호 확인과 닉네임 중 코드에서 먼저 선언된 비밀번호 확인에서 focus가 되는 것을 확인할 수 있습니다.


import React, { useRef } from "react"; function App() { const inputRef = useRef([]); const inputCheck = (e) => { e.preventDefault(); if (inputRef.current["email"].value === "") { console.log("이메일을 입력해주세요"); inputRef.current["email"].focus(); return; } else if (inputRef.current["pw"].value === "") { console.log("비밀번호를 입력해주세요"); inputRef.current["pw"].focus(); return; } else if (inputRef.current["pwcheck"].value === "") { console.log("비밀번호 확인을 입력해주세요"); inputRef.current["pwcheck"].focus(); return; } else if (inputRef.current["name"].value === "") { console.log("이름을 입력해주세요"); inputRef.current["name"].focus(); return; } else if (inputRef.current["nickname"].value === "") { console.log("닉네임을 입력해주세요"); inputRef.current["nickname"].focus(); return; } }; return ( <form style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: "10px", }} onSubmit={inputCheck} > <h2>프로필 설정</h2> <label> 이메일: <input type="email" ref={(el)=>(inputRef.current["email"]=el)}></input> </label> <label> 비밀번호: <input type="password" ref={(el)=>(inputRef.current["pw"]=el)}></input> </label> <label> 비밀번호 확인: <input type="password" ref={(el)=>(inputRef.current["pwcheck"]=el)} ></input> </label> <label> 이름:{" "} <input type="text" ref={(el)=>(inputRef.current["name"]=el)}></input> </label> <label> 닉네임: <input type="text" ref={(el)=>(inputRef.current["nickname"]=el)}></input> </label> <button type="submit" style={{ width: "180px" }}>회원가입하기</button> </form> ); } export default App;