Web: React Ref와 useRef 알아보기

Heechan
HcleeDev
Published in
10 min readMar 25, 2022

--

Photo by Mark König on Unsplash

React를 사용하다보면 다양한 Hook을 쓰게 된다. 가장 많이 쓰게 되는건 useStateuseEffect 가 아닐까 싶다. 지난 2주간 열심히 설명한 상태 관리 라이브러리가 있다 하더라도, 간단하거나 그다지 사이즈가 크지 않은 상태는 useState 를 사용해 처리하는 경우가 많다.

그러던 중 자주 사용하지는 않지만 어쩌다 가끔 사용하게 되는 useRef 라는 녀석이 눈에 띄었다. 이번 주는 이 useRef 란 무엇인지, 그 근본이 되는 React의 Ref란 무엇인지 간단하게 알아보자.

React의 ref?

개발 중 종종 화면에 직접 접근해야 하는 경우가 있다. 가장 많이 예시로 드는 것은 특정 Input에 focus 설정을 할 때다. 이런 경우 state로 해결할 수 있는 것이 아니라, 해당 Input에 직접 접근을 해서 뭔가 수정해줘야 한다.

이 Input이라는 요소에 접근하기 위해서는 화면에 대한 정보가 담겨 있는 DOM에 직접 접근해야 한다. 이 DOM에서 우리가 원하는 Element, 즉 DOM 노드를 찾고 이에 접근해 직접 값을 바꾸는 방식을 취해야 한다.

사실 일반적인 HTML에서는 이 작업을 document.getElementById() 를 이용해 수행한다. 이게 딱 위에서 설명한 것과 흡사하다. DOM에 접근해서, 우리가 원하는 Element를 Id를 통해 찾아서 가져온다.

하지만 React를 통해 화면을 구성하면 기본적인 HTML 요소말고도 React로 만든 컴포넌트도 사용하게 된다.

React는 HTML의 DOM 노드와 React Element에 접근할 수 있도록 ref 를 제공한다. 위 설명에도 나와있는 것처럼, 특히 클래스 컴포넌트의 경우는 상위 컴포넌트가 하위 컴포넌트에게 뭔가 데이터를 전해줄 때 props 를 이용해야 한다. 하지만 그 상황이 아니라 직접 조종해야 하는 경우 직접 접근을 할 수 있도록 어떤 포인트를 찍어주는 느낌의 ref 를 이용할 수 있다.

간단한 사용 예시를 보자. 예시들은 공식 문서에서 대로 가져왔다.

class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}

React.createRef() 를 이용해 ref를 생성할 수 있다. 생성된 ref는 myRef 에 할당되었고, 이는 divref attribute에 추가된다. 이렇게 되면 이제 우리는 myRef 를 통해서 저기 있는 div 에 접근할 수 있게 되었다.

위는 DOM Element에 사용한 것이고, 다른 React의 클래스 컴포넌트를 확인해보자.

class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}

componentDidMount() {
this.textInput.current.focusTextInput();
}

render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}
class CustomTextInput extends React.Component {
// ...
}

이렇게 사용자가 만든 React 클래스 컴포넌트의 경우에도 ref attribute를 통해 지정할 수 있다. 여기서 흥미로운 점은 componentDidMount 에 있다. 이 경우 페이지가 켜졌을 때 자동으로 특정 텍스트 인풋에 focus가 가있도록 해주는 코드다. 주목할 것은 this.textInput.current 인데, 어떤 ref를 통해 그 컴포넌트에 직접 접근하기 위해서는 current 를 사용해야 한다.

여기서 왜 Ref라는 이름이 붙었는지 생각해보자.

흔히 Reference Type 이런 이름으로 익숙할 Reference는, 어떤 주소에 대한 정보를 담고 있다는 것을 암시한다. 여기서 Ref는 { current: ? } 이런 느낌의 객체와 비슷한 존재라고 생각할 수 있는데, 여기서 current 에는 우리가 원하는 어떤 DOM 노드, 혹은 클래스 컴포넌트의 ‘인스턴스’에 대한 주소값을 저장하게 된다.

위에서 <div ref={myRef} /> 이런 식으로 ref attribute를 통해 넘겨줄 때 current 에 이 DOM 노드에 대한 주소값을 저장하게 된다.

따라서 객체에 주소를 저장하는 것이기 때문에 Ref라고 이름을 붙인 것으로 보인다.

그런데 계속 클래스 컴포넌트를 강조한 이유가 있다. 이 Ref는 주소를 저장하고, 그러려면 ‘인스턴스’가 있어야 한다. 하지만 함수형 컴포넌트를 사용할 경우에는 인스턴스가 존재하지 않는다. 따라서 아래와 같은 방식은 불가능하다.

function MyFunctionComponent() {
return <input />;
}

class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
return (
<MyFunctionComponent ref={this.textInput} />
);
}
}
// 불가능한 코드

useRef Hook과 활용법

하지만 이런게 있는데 함수형 컴포넌트에서 사용하지 못하면 섭섭할 것이다.

클래스 컴포넌트에서의 state를 함수형 컴포넌트에서도 쉽게 사용하기 위해 useState 라는 Hook이 생겨난 것처럼, Ref도 useRef 가 있다.

공식 문서에서는 위처럼 소개하고 있다. useRef(initialValue) 처럼 초기값을 집어넣어주면, current 에 해당 값에 대한 정보가 들어간 Ref 객체가 돌아올 것이다. 그냥 { current: initalValue } 가 오는거다.

useRef 가 주는 값은 current 라는 값을 담고있는 상자라고 생각하면 된다. 굳이 useState 로 표현해보자면 const [ref, _] = useState({ current: ? })ref 가 될 것이다.

위 문서에서 흥미로운 점은 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지될 것이라는 문장이다.

기본적으로 생애주기는 생길 때, 업데이트될 때, 없어질 때의 세 가지로 나뉘어서 생각할 수 있는데, 이 전반의 생애주기에서 같은 객체를 반환해준다는 것이다.

안전한 곳에 Ref 객체를 따로 만들어두고, 그 객체에 접근할 수 있는 참조만 저장하고 있다고 생각하면 이해가 된다. 그리고, 이 특징 때문에 컴포넌트 내에서 두 가지 활용법이 생긴다.

먼저 일반적인 사용 예시를 한 가지 확인해보자. 아래 예시는 지금까지 말했던 것처럼 DOM 노드에 접근하기 위해 사용하는 경우다.

function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

함수형 컴포넌트로 만들어진 버튼이다. 처음에 useRef(null) 을 통해 currentnull 을 담은 채로 Ref 객체를 만들었다. 그 후 input 에다가 ref attribute로 넘겨줌으로써 해당 DOM 노드를 current 에 저장하도록 한다.

이렇게 저장한 DOM 노드는 onButtonClick 에서 사용하고 있다. inputEl.current.focus() 를 보면 알 수 있듯, Ref 객체가 가지고 있는 current 에 직접 접근해, 즉 input DOM 노드에 접근해 focus() 되도록 하고 있는 모습이다.

useRef 로 만들어낸 값의 특징은, 값이 변경되어도 리랜더링이 되지 않는다는 것이다.

위에서 살짝 언급한 것처럼, useState로 표현한다면 const [ref, _] = useState({ current: ? })ref 가 우리가 사용하고 있는 useRef 의 결과값이라고 볼 수 있다.

여기서 리랜더링이 되지 않는 이유를 살펴보자면, 일단 useRef 는 세터가 없다. useState의 경우를 생각해보면, const [value, setValue] = useState(0) 이렇게 만들었을 때, setValue 라는 세터를 이용해 값을 업데이트해야 리랜더링이 제대로 된다. 하지만 useRef 는 그런 세터의 존재가 없다.

그리고 한 가지 더 생각해보자면, 우리가 직접 current 에 접근해 그 값을 바꾸는 경우, 객체 내부의 값을 바꾸는거지 객체 자체를 바꾸는 것이 아니다. 이렇게 객체 내부로 직접 접근해서 바꾸면 useState로 만든 경우에도 확인이 힘들 것이다.

useRef 는 값을 담고 있는 ‘상자’를 쳐다보고 있지, 상자 안에 들어있는게 바뀐다고 해서 다른 값이라고 인지하진 않는다고 생각하면 좋을 것 같다.

따라서 이 특성을 이용한 두 번째 활용 방법은, 변경할 때 딱히 리랜더링이 필요없는 변수 관리에 사용된다.

function RefButton() {
const numRef = useRef(0);
const onButtonClick = () => {
numRef.current += 1;
request();
};
const request = () => {
// ...
axios.get( ... , { num: numRef.current })
// ...
}
return (
<>
<button onClick={onButtonClick}>Add One</button>
</>
);
}

약간 이런 느낌이라고 볼 수 있겠다. 만약 우리가 어떤 숫자를 계속 늘려가면서 서버에 요청을 보내야 하는데, 이 경우는 화면에 숫자를 보여주고 있지 않음으로 딱히 리랜더링이 필요하지 않다. 그냥 서버에서 보내는 로직에서만 사용하고 있으니…

이런 경우 useRef 를 이용해 우리가 원하는 값을 관리하도록 하고, numRef.current 로 접근해 사용하거나 값을 수정할 수 있다.

결론

아무튼 useRef 는 함수형 컴포넌트에서 두 가지 용도로 사용할 수 있다. 다만 느낌이 정말 필요할 때가 아니면 굳이 남발할 이유까진 없어보인다. DOM 노드 선택 같은 경우는 어쩔 수 없이 써야겠지만, 추가적인 랜더링이 필요없는 경우를 신중히 생각해 사용하면 좋을 것 같긴 하다.

원래는 useRef 에 대해서만 쓰려고 했는데 자연스레 Ref에 대해서 알게 되어서 좋았다. 이외에도 forwardRef인가 뭔가 다양하게 있긴 한데, 그건 또 다음 기회에 공부해봐야 겠다.

참고한 것

--

--

Heechan
HcleeDev

Junior iOS Developer / Front Web Developer, major in Computer Science