[React] 리액트를 처음부터 배워보자. — 07. createRef와 useRef 그리고 useImperativeHandle

Stark Studio
Cross-Platform Korea

--

이 글을 남기는 계기는 Dan Ambramov의 블로그 Overreacted를 보며 React.js에 대해 더 깊이 공부할 필요성을 느꼈기 때문이다.

HTML, CSS, Javascript를 공부하고 바로 React를 시작했을 때 가장 어려웠던 개념이 ref였다.

당시에는 Javascript를 제대로 이해하고 있지 않아 ref 에 대한 공식문서 설명을 이해하기 보다는 ref 라는 기능을 쓰기에 바빴다.

Ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공합니다.

- React Document(리액트 공식문서)

1년간 실무를 하며 ref 를 자주 사용하며 ref 기능의 다양한 사용 예시를 경험하였고, 해당 기능에 대해 다시 한 번 공부하고 싶다는 생각이 들었다.

React 라이브러리는 ref를 왜 만들었을까?
이 글은 해당 질문을 중심으로 ref와 연관된 아래 개념들을 정리하는 글이다.

01. DOM API(querySelector, getElementById)

02. Mutable(변하기 쉬움)과 Immutable(불변성)

03. Declarative(선언형)과 Imperative(명령형)

React에서 Ref

Vanilla Javascript에서는 DOM 객체에 접근하기 위해 querySelectorgetElementById API를 사용해야 한다.

반면, React는 아래와 같은 이유로 DOM API를 이용한 컴포넌트 제어 방식을 권장하지 않는다.

01. React를 이용한 웹 소프트웨어에서 데이터는 State로 조작되기에 DOM API와 혼합해서 데이터 및 조작을 할 경우 디버깅이 어려워지고, 유지보수가 어려운 코드가 된다.

02. map 메소드를 이용해 렌더링 되는 리스트 형태의 Element는 같은 ID를 가지기에 특정 DOM 객체를 querySelector, getElementById로 판별하기 어렵다.

따라서, React는 DOM API를 이용한 컴포넌트 제어 대신 ref라는 기능을 제공한다. ref를 통해 DOM 객체에 대한 직접적인 참조 주소를 반환 받아 HTML DOM 메소드 및 속성을 이용할 수 있다.

[그림 01] HTML DOM 속성에 접근하는 ref의 예시. clientHeight는 HTML DOM의 주요 속성이다.

React에서는 ref기능을 이용하기 위해 React.createRef()useRef() 를 제공한다.

이 둘을 간략하게 구분하면 Class 컴포넌트에서는 createRef()를 사용하고 Functional 컴포넌트에서는 useRef()를 사용한다.

이 둘에 대한 차이는 잠시 미뤄두고 각 기능은 다음과 같이 사용한다.

[그림 02] Class 컴포넌트에서 React.createRef()를 써서 ref를 만드는 예시. current라는 속성으로 값을 조회함

이때, 중요한 것은 createRef()useRef()자체는 Mutable Object를 만드는 기능이라는 점이다.

ref는 componentDidMount나 useEffect가 실행되기 전에 할당됩니다.
컴포넌트가 마운트 될 때 할당되며, 언마운트 될 때 Null을 할당합니다.

즉, React Component의 인스턴스나, React DOM의 ref 속성에 해당 ref 참조(this.ref)를 선언하지 않으면 그저 Mutable Object일 뿐이다.

이를 확인하기 위해 createRef의 실제 구현체를 확인해보자.

[그림 03] React.createRef()의 실제 구현체. Object가 Reference 타입의 자료형임을 이용했다.

실제로 createRef()의 구현체를 확인하면 Javascript의 Object가 Reference(참조) 타입의 변수라는 것을 이용한 간단한 코드로 구현된 것을 확인할 수 있다.

즉, createRef()useRef()로 생성한 참조주소를 JSX의 ref 속성으로 선언해주어야 해당 컴포넌트 혹은 DOM Element에 대한 참조를 얻을 수 있다.

createRef와 useRef

앞서 Class 컴포넌트에서는 createRef()를 Functional 컴포넌트에서는 useRef()를 쓴다고 언급했다. 이 둘의 차이는 무엇일까?

이 둘의 차이를 이해하기 위해서는 Class 컴포넌트와 Functional 컴포넌트의 리렌더링 차이를 알아야 한다.

글의 진행을 위해 간단하게 설명하면 Class 컴포넌트는 인스턴스를 생성해 render 메소드를 호출하는 방식으로 리렌더링을 한다면, Functional 컴포넌트는 해당 함수를 리렌더링 마다 다시 실행한다.

위와 같은 차이는 Functional 컴포넌트에서 createRef() 메소드를 이용하기 어렵게 한다. 아래 예시 코드를 보자.

[그림 04] Functional 컴포넌트에 createRef를 쓸 경우 Component가 렌더링 될 때마다 ref가 생성된다.

createRef()는 앞서 확인한 것과 같이 그저 Mutable Object를 생성하는 기능이기에 Functional 컴포넌트에서 리렌더링 될 때마다 새로운 Mutable Object를 생성하게 된다.

즉, ref를 이용해 Mutable Object를 만들어도 state가 변경되면 createRef()가 다시 호출되며 새로운 ref를 반환한다.

[그림 05] useRef의 구현체로 별도의 Dispatcher에서 해당 컴포넌트의 Hook을 관리함

이와 같은 문제를 해결하기 위한 방법으로 React에서는 useRef()라는 기능을 제공한다.

useRef는 Hook의 Dispatcher를 통해서 동작하는데 내부적으로 mountRefupdateRef 를 이용해 동작하는 것을 확인할 수 있다.

[그림 06] useRef의 내부 동작을 담당하는 mountRef와 updateRef의 구현체

mountRefupdateRef 구현체를 통해 두 가지 사실을 알 수 있다.

첫번째, mountRef는 현재의 Hook에 대한 정보를 보관하는 Object의 memoizedState라는 속성에 ref를 저장하는 방식으로 구현됐다.

두번째, updateRef는 컴포넌트가 업데이트 되어 리 렌더링이 발생할 때 memoizedState를 반환하는 방식으로 구현됐다.

useImperativeHandle와 ForwardRef

React는 기본적으로 Declarative(선언형) 패러다임을 따른다.
즉, 뷰의 동작에 대해 하나 씩 명시하는 것이 아닌 stateprops의 변경에 따라 변하는 뷰를 선언하는 방식으로 개발된다.

하지만, Ref 기능의 경우 기능을 조작하기 위해 Imperative(명령형) 패러다임을 따른다. 아래의 코드를 보자.

[그림 07] Imperative(명령형) 패러다임을 따르는 ref 기능

특정 동작에 대한 메소드를 구현하고 Imperative(명령형) 패러다임으로 개발하는 것을 확인할 수 있다.

ref를 통한 이런 기능은 Class 컴포넌트에서는 기본적으로 동작하지만 내부 메소드를 정의할 수 없는 Functional 컴포넌트에서는 별도의 기능이 필요하다.

React에서는 Functional 컴포넌트에서 ref를 통한 접근을 위해forwardRef()useImperativeHandle() 를 제공한다.

[그림 08] useImperativeHandle과 forwardRef를 이용한 명령형 프로그래밍

forwardRef() 는 Functional 컴포넌트에 ref 속성을 이용할 수 있도록 하는 기능으로 해당 함수로 Wrapping 된 컴포넌트는 위와 같이 ref를 매개변수로 받을 수 있게 된다.

React 내부 WorkTag를 확인하면 forwardRef()를 통해 생성된 컴포넌트는 일반 FunctionComponent 태그와 차별을 두는 ForwardRef 태그로 명시된다.

[그림 09] React 내부적으로 동작을 구분하기 위해 사용하는 work tag. ForwardRef를 통한 컴포넌트를 분리함

useImperativeHandle()는 이름 처럼 Imperative(명령형) 동작에 대한 Handler를 제공한다.

즉, ref.current의 속성으로 접근할 수 있는 메소드에 대해 구현하는 부분이다. 이를 통해 부모 컴포넌트에서 자식 컴포넌트의 state를 조작할 수도 있고, 특정 애니메이션을 트리거 할 수도 있다.

결론

이 글을 통해 React에서 제공하는 ref 기능에 대해 다음과 같은 사실을 배울 수 있었다.

01. Ref는 DOM Element 객체나 React Component 인스턴스를 직접 조회할 수 있다.

02. useRef()createRef()는 current 속성을 가지는 Mutable Object를 반환한다.

03. Ref기능은 React의 기본 패러다임인 Declarative(선언형)이 아닌 Imperative(명령형) 패러다임의 기능이다.

이런 ref의 특성을 이용하면 특정 DOM의 API를 직접적으로 사용할 수 있고, 렌더링 없이 변경이 필요한 데이터를 관리할 수 있다.
또한, 자식의 state에 부모가 접근할 때나 state로 제어하지 않는 비제어 컴포넌트 를 사용할 때도 ref를 이용할 수 있다.

React를 통해 개발을 진행하다 보면, stateprops 구조의 규칙을 지키면서 개발하기 어려운 요구 사항이 생길 수 있다.

이 글을 통해 그런 요구사항에 대해 "ref 기능을 이용한 색 다른 해결 방법이 있지 않을까?” 고민해 볼 수 있는 관점을 얻은 것 같다.

맺음말

처음 React Native로 앱 개발을 했을 때, 웹을 개발할 때 보다 ref를 많이 써서 당황한 적이 있다.

처음 React를 배울 때 ref는 비중이 높게 다뤄지지 않았기 때문에 자연스레 이 방법이 잘못된 것은 아닌지, 내가 잘 쓰고 있는지에 대한 고민을 많이 했다.

React Native 라이브러리에는 네이티브 기능을 이용하기 위해 ref를 많이 쓴다. 예를 들면 FlatList 의 특정 항목으로 스크롤 이동을 할 때 사용한다.

ref를 통한 조작이 익숙해질 때, 문득 “특정 UI 의 기능 명세가 확실하다면, useImperativeHandlerForwardRef를 통해 UI의 동작 기능을 ref의 메소드로 관리하는 것이 컴포넌트의 모듈화에 더 유용하지 않을까?” 라는 생각이 들었다.

물론 이 방식이 React의 Declarative(선언형) 패러다임에 반하는 것이지만 패러다임에 고정된 생각보다는 사용성의 관점으로 고민이 되는 부분이었다.

혹시나 이 부분에 대한 이유나 혹은 이렇게 개발해 본 경험이 있는 React 개발자가 있다면, 댓글로 남겨주면 많은 사람들에게 도움이 될 것 같다.

참고자료

지난글

--

--