본 포스팅은 'React Hook' 에 대한 시리즈 게시글 중 3번째 게시글로, useRef에 대해 중점적으로 다루고 있습니다!
우리가 React 로 컴포넌트들을 만들다 보면 컴포넌트 내부에서 어떤 값들을 저장하고, 유지하고, 그 값들로 계산해야 하는 일들이 자주 발생한다. 처음에 React 를 배울 때는 고민이 되게 많았던 지점이었다. 특히나 React 입문을 하게 되면 가장 먼저 배우는 것이 useState()
인 만큼,
어 React 에서는 그냥 필요한 값들은 전부 state 에 저장하면 되지~! 😃
라고 생각한 적도 있었다.
그러나 state 를 매번 사용하면 이 값이 변할 때마다 필요하지 않아도 매번 rerender 가 일어나는 문제가 생긴다. 성능 저하를 불러오는 것이다.
그러면 다음과 같이 생각할 수도 있다.
엥 그러면 그냥 컴포넌트를 re-render 시키지 않는 그냥 JS 변수 쓰면 되는 것 아닌감? 🙋🏻
JS 변수를 사용하면 state 가 아니기 때문에 당연히 컴포넌트 re-render 는 일어나지 않는다. 그런데 더 큰 문제가 발생한다.
다음의 코드를 보자.
import { useRef } from "react";
function UseRefStudy() {
let myVar = 0;
const [foo, setFoo] = useState(0);
function incrementMyVar() {
myVar += 1;
console.log("myVar: ", myVar);
}
function decrementMyVar() {
myVar -= 1;
console.log("myVar: ", myVar);
}
return (
<div>
<h1>UseRef 스터디</h1>
<div className="container">
<div className="box">
<h2>일반 변수 사용</h2>
<p>myVar 의 값: {myVar}</p>
<p>Foo 의 값: {foo}</p>
<button onClick={incrementMyVar}>myVar 증가시키기</button>
<button onClick={decrementMyVar}>myVar 감소시키기</button>
<button
onClick={() => {
setFoo(1);
}}
>
Foo 를 1로 변경
</button>
</div>
</div>
</div>
);
}
export default UseRefStudy;
위 코드는 각 버튼 클릭을 통해 myVar
이라는 변수를 변경시킨다. 당연히 myVar
는 state 가 아니므로 re-render 을 야기시키지 않아, 화면에 업데이트는 되지 않는다. (그래도 허전해서 myVar 의 값
이렇게 넣어두었다 😆)
따라서 증가를 시켜도 console 상에는 제대로 myVar
의 값이 제대로 표시되는 반면, 화면에는 계속 myVar
의 값은 0 인 상태로 남아있다. 그렇다면 이 컴포넌트를 강제로 re-render 시키면 myVar
이 설정된 대로 나올까?
그것도 아니다. 왜냐하면 결국
re-render 시킨다는 것은 결국 컴포넌트 (즉, 함수) 를 재호출하는 것!
즉, 평범한 JS local variable (지역변수) 인myVar
의 값이 함수 재호출 시 보존될 리 없다.
즉, 아래와 위 실행 이미지와 같이 Foo
라는 state 를 1로 변경하게 만들어 컴포넌트 전체를 임의로 re-render 되게 한 후, 다시 console 에 찍힌 myVar
을 보면 원래 기존의 6
에서부터 시작해서 7
이 표시되는 대신, 이미 다시 컴포넌트가 호출되면서 0
으로 초기화돼버린 상태에서 시작하기에, 다시 1
부터 시작하는 것을 알 수 있다.
즉, re-render 이 필요 없는데에 JS 변수를 사용하는 것에 가장 큰 문제는
컴포넌트 re-render 사이에 값이 보존되지 않는다
는 것이다!
useRef()
은 state 와 state 가 아닌 일반 JS 변수의 특징을 조금씩 가져온 형태이다. 근본적으로 useRef()
는 다음 두 가지 상황에 대응하기 위해 만들어졌다고 볼 수 있다.
즉,state 는 re-render 되더라도 값을 그 값이 보존되는 데에 비해, 값이 변하면 컴포넌트를 무조건! re-render 시켜버리고, 그렇다고 일반 JS 변수를 쓰자니 re-render 사이에 값이 보존이 안 되어 초기화되어버리는 문제가 생겼다.
다음의 코드를 보자
import { useRef, useState } from "react";
function UseRefStudy() {
const myRef = useRef(0);
const [foo, setFoo] = useState(0);
// myRef 관련
function incrementMyRef() {
myRef.current += 1;
console.log("myRef.current: ", myRef.current);
}
function decrementMyRef() {
myRef.current -= 1;
console.log("myRef.current: ", myRef.current);
}
return (
<div>
<h1>UseRef 스터디</h1>
<div className="container">
<div className="box">
<h2>ref 사용</h2>
<p>myRef.current 의 값: {myRef.current}</p>
<p>Foo 의 값: {foo}</p>
<button onClick={incrementMyRef}>myRef 증가시키기</button>
<button onClick={decrementMyRef}>myRef 감소시키기</button>
<button
onClick={() => {
setFoo(1);
}}
>
Foo 를 1로 변경
</button>
</div>
</div>
</div>
);
}
export default UseRefStudy;
위 코드는 ref
를 이용한 케이스이다
useState()
와 마찬가지로 초기값을 인자로 받는다!
우선 아래 실행 결과를 먼저 보자.
처음에 버튼을 눌러 myRef
를 증가시킨다. (정확히는 myRef.current
를 증가시킨다. 이 myRef.current
가 왜 나오는지는 곧 설명할 것이다! 😃)
당연히 ref 도 state 가 아니므로, 컴포넌트를 re-rendering 시키지 않아 화면은 변하지 않는다. 대신 여기서도 console 상에 이 myRef.current
값이 변했다고 표시가 된다. 여기까지는 이전의 JS 변수와 동일하다.
그런데 이 다음이 다르다. 이렇게 하고 나서 이전과 같이 Foo
라는 state 를 1로 변경하게 만들어 컴포넌트 전체를 임의로 re-render 되게 해 보자.
우선 바로 눈에 보이는 변화는 일반 변수로 사용했을 때와는 달리 화면 자체에 myRef.current
값이 실제 console 상의 이때까지 우리가 6번 버튼을 클릭하여 증가시킨 바로 그 값으로 업데이트가 되었다. 즉,
re-render 시킨다는 것은 결국 컴포넌트 (즉, 함수) 를 재호출하는 것!
임에도 불구하고,
ref 에 저장했던 값이 서로 다른 함수 호출 사이에 보존 이 되었던 것이다!
즉,
state 처럼 컴포넌트 re-render 사이에 값들이 보존은 되지만, state 와 달리 ref 자체의 값 변화에 의해 re-render 은 발생시키지 않는 것이 바로
ref
이다.
그렇다. 우리는 이 부분을 짚고 넘어가야 할 필요가 있다.
ref 에 current 가 왜 붙는지는 문법을 먼저 보면 알 수가 있다. 우리가 ref 의 값에 접근할 때
myRef.current
와 같이 그냥 myRef
가 아니라 항상 .current
를 붙여줬다.
이로 미루어 보아, myRef
자체는 JS object 이고, 그 안에 current
라는 속성이 있으며, 우리는 그 current
속성에 해당하는 값 을 사용하고 있었던 것이다.
즉,
ref 는 JS object (JS 객체) 이다.
그 객체의 구조는 다음과 같다.
ref = { current: 우리가 저장하고 싶은 값 }
매우 단순하다 😅.
아니 이렇게 할 거면 그냥 저장하지 왜 이렇게 했을까?
그 이유는 이렇게 생각해보면 쉽다.
ref 의 핵심은 컴포넌트 re-render 간 어떤 데이터를 잘 보존하는 것이다.
예를 들어, 이 current
속성을 포함하는 객체를 '보물상자' 라고 생각하고, 그 안에 '상자' 안에 들어가는 '보물'을 우리가 current
속성 안에 저장하는 데이터라고 생각해 보자.
그러면 결국 이 '보물' 을 잘 지키려면, '보물상자' 만 잘 지키면 된다. 그리고 내가 나중에 이 '보물상자' 에 원래 있던 보물이 아니라 다른 보물을 넣는다 하더라도, 그 '보물상자' 만 잘 지켜지는 한, 나의 보물은 안전함을 보장할 수 있다.
JS 에서도 역시나 mutable 한 자료형인 object 에 저장하고 싶었던 것이다. 그래서 이 object 만 잘 보관하면 그 안의 데이터는 덩달아 잘 보존이 되는 것이다. 그런데 객체의 mutable 한 속성은 이용하고 싶은데, 그렇다고 객체 안에 데이터를 넣을 때 key 값 (속성 값) 없이 value 만 넣을 수는 없는 노릇이다. 하나의 데이터를 객체에 넣더라도, key 값이 반드시 필요하다. 즉 아래와 같이 객체를 만들 수는 없다는 것이다.
const ref = { myData } // ?????
하나를 넣더라도
const ref = { foobar: myData }
이렇게 넣어야 문법에 맞는 객체가 된다. 즉, React team 에서 이 foobar
에 해당하는 자리에 넣을 그냥 '아무 key (속성)값'으로 쓰기로 한 것이 바로 current
이고, 결과적으로 아래와 같은 형태가 된 것이다.
const ref = { current: myData }
아주... 좋은 질문이다! 😅
원론적인 답변을 하자면,
컴포넌트 re-render 사이에 보존되어야 할 값
을 저장하는 데에 쓰면 된다.
... 라고 하면 너무 당연하니까 몇 가지 사용 예시를 만들어 보자!
이 예시 또한 실용적으로 사용을 할 일은 잘 없을 것 같지만, 이런 로직을 이용할 수가 있다는 demo 목적으로 추가해 보았다!
다음의 코드를 보자.
import { useRef, useState, useEffect } from "react";
function UseRefStudy() {
const myRef = useRef(0);
const [foo, setFoo] = useState(0);
const renderCount = useRef(0);
useEffect(() => {
renderCount.current += 1;
}, [foo]);
// myRef 관련
function incrementMyRef() {
myRef.current += 1;
console.log("myRef.current: ", myRef.current);
}
function decrementMyRef() {
myRef.current -= 1;
console.log("myRef.current: ", myRef.current);
}
return (
<div>
<h1>UseRef 스터디</h1>
<div className="container">
<div className="box">
<h2>ref 사용</h2>
<p>myRef.current 의 값: {myRef.current}</p>
<p>Foo 의 값: {foo}</p>
<button onClick={incrementMyRef}>myRef 증가시키기</button>
<button onClick={decrementMyRef}>myRef 감소시키기</button>
<button
onClick={() => {
setFoo(foo === 0 ? 1 : 0);
}}
>
Foo 를 1로 변경
</button>
<button
onClick={() => {
console.log("renderCount: ", renderCount.current);
}}
>
render 횟수 보기
</button>
</div>
</div>
</div>
);
}
export default UseRefStudy;
위 코드에서는 renderCount
라는 ref 가 하나 더 추가되었다.
그리고, foo
를 toggle 형식으로 1과 0을 왔다갔다 하게 함으로써 계속 re-render 시킬 수 있도록 만들었다.
즉, 지금까지 render 횟수를 저장하는 ref 이다. ref 를 사용하기 좋은 케이스이다.
아래 사용 영상을 보자
renderCount
가 1 이다. 계속 눌러도 state 가 업데이트 된 적이 없으므로, 계속 1이다. myRef
를 (정확히는 myRef.current
를) 증가시키면 myRef.current
의 값은 증가된 값으로 console 에 찍히고, 그 다음 컴포넌트를 임의로 re-render 시키기 위해 foo
라는 state 를 1로 변경한다. renderCount
를 console 에서 보면 1 증가한 2가 된 것을 알 수 있다. 즉, state 인 foo
값이 업데이트 될 때마다 useEffect
안에서 myRef.current
를 증가시키고 있기 때문이다! (useEffect
는 다음 포스팅에서 자세히 알아보자!)이렇게 이용을 해 볼 수 있다. 하지만, 실제로 이 ref
가 더 자주 쓰이는 데는 아무래도 DOM Element 를 직접 control 하고 싶을 때일 것이다. 다음 케이스는 아래와 같다.
훨씬 더 실용적인 사용 예시이다.
다음 코드를 보자.
import { useRef } from "react";
function UseRefStudyFocus() {
const inputRef = useRef();
function focusInput() {
inputRef.current.focus();
}
return (
<div>
<h1>UseRef:: focus Input</h1>
<div className="container">
<div className="box">
<h2>input focus 시키기</h2>
<input ref={inputRef} />
</div>
<div className="box">
<button onClick={focusInput}>입력하기!</button>
</div>
</div>
</div>
);
}
export default UseRefStudyFocus;
(참! 그리고 이때까지 코드에서 눈치챘을 수도 있지만, useRef()
를 사용한 ref 들은 컨벤션상 ~Ref
를 붙여주는 것이 일반적이다. 마치 useState
에서 두 번째 setter 함수를 받을 때 set~
으로 시작하곤 하는 그런 것!)
위의 코드는ref
에 어떤 string 이나 number 같은 단순한 데이터가 아니라, DOM element 그 자체 (지금의 경우에는 <input />
이 들어간 케이스이다.
중간에 <input ref={inputRef} />
에서 <input />
이라는 DOM Element 에 어떠한 '핸들' 역할을 해서 그 DOM element 를 '잡을' 수 있게 해 주는 ref
속성에 inputRef
를 추가했다.
이렇게 되면, inputRef.current
로 우리가 일반적으로 <input />
에서 쓰는 .focus()
라든지, .blur()
등의 method 를 사용할 수 있다.
이렇게 해서 아래 입력하기
버튼을 누르면 focusInput()
함수가 실행되며, 이 함수 내부에서 inputRef.current.focus()
를 부르며, 내부적으로 이 input 컴포넌트를 실행시켜 준다. 아래 실행 결과를 보자.
아니 이걸 왜 하는거임? 그냥 text field 직접 눌러서 하는게 더 직관적이잔슴
이라고 하는 분들이 계실 것 같다. 😅 (본인도 그렇다)
그래서 이것이 유용하게 쓰일 수 있으려면, 다음과 같이 useEffect()
를 활용하는 것도 한 방법이다. 아래의 변경된 코드를 한번 보자.
import { useRef, useEffect } from "react";
function UseRefStudyFocus() {
const inputRef = useRef();
function focusInput() {
inputRef.current.focus();
}
// 추가된 코드
// 페이지 로딩 시 자동으로 input focus
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<h1>UseRef:: focus Input</h1>
<div className="container">
<div className="box">
<h2>input focus 시키기</h2>
<input ref={inputRef} />
</div>
<div className="box">
<button onClick={focusInput}>입력하기!</button>
</div>
</div>
</div>
);
}
export default UseRefStudyFocus;
이번에는 useEffect()
가 추가되면서, 이 안에서 inputRef.current.focus()
를 불러주었다. 아직 useEffect()
에 대한 자세한 포스팅은 없었지만, 위와 같이 useEffect()
다음 빈 배열을 하나 넣어주면, component 가 최초 render 된 직후 한 번만 실행된다!
즉, 컴포넌트가 render 된 직후, input
field 에다가 focus 를 잡아주는 것이다. UX 적인 측면에서 봤을 때 이러한 요소가 필요할 수도 있는 것이다! 그러면, 사용자가 별도의 클릭 없이도, 바로 input field 에서 키보드로 입력을 진행할 수 있다.
이것을 잘 활용하면, 이를테면 신용카드 번호 입력란과 같이, 특정 길이나 형식의 문자열이 입력된 경우, 다음 입력 칸으로 자동으로 넘어가게 하는 등의 기능도 구현할 수 있다!
그런데 왜 ref
에 DOM element 를 지정하는 것일까?
우선 state
에 저장하면, 이 요소가 변할 때마다 re-rendering 이 일어나므로 변화무쌍(?) 한 DOM element 를 두기에는 성능 및 기능면에서 큰 이슈가 있다.
그렇다고 일반 변수에 저장하면, 컴포넌트가 re-render 될 때 마다, 새롭게 이 DOM element를 불러오게 되서 DOM element 자체가 component re-render 사이마다 보존이 되지 않는다.
ref
는 어떠한 DOM element 도 직접적으로 다룰 수 있게 해 주기 때문에, 초기에 ref
를 배우게 되면, 이를 남용하는 경우가 발생한다. 그리고 이는 React 의 dataflow 컨셉에 맞지 않다.
(부모 컴포넌트에서 자식 컴포넌트로 데이터 플로우가 일어나는 단방향 데이터 바인딩!) 뿐만 아니라, 단순히 ref
만으로는 state
와 달리 이 ref
의 변화를 react 가 내부적으로는 감지할 수 없는 문제도 있다.
React 공식 문서에서도 ref
는 정말 부득이한 (imperative) 경우에만 사용하라고 하고 있으며, ref
를 사용하지 않고 state 나 다른 callback 함수들을 전달함으로써 설계가 가능하다면, 그렇게 하는 것을 권장하고 있다.
inputRef.focus()
와 같이 기존에 있는 element 에서는 우리가 무언가를 변경할 수 없으므로, '부득이' ref 를 사용하긴 하나, 이 경우에도 최대한 사용을 자제해야 하는 것은 맞다.
ref 와 관련한 이야기는 앞서 이야기 했듯이, 단방향 데이터 바인딩, controlled VS uncontrolled element, callback ref 등 다룰 내용이 매우 많다.
여기서는 아주 기본적인 내용과 개념만 소개하였다. useRef()
와 관련해서 더욱 심화된 내용을 다루는 포스팅도 후속 포스팅으로 업로드할 예정이다!