리액트 공식문서 Manipulating the DOM with Refs 를 읽고 내 마음대로 정리해보는 포스트
저번 챕터에서 Ref
에 대한 부분을 공부했었는데
이전 부분까지만 봤을 때는
아 ~ ㅋㅋ 그냥
useState
인데setter function
안쓰고 직접적으로 값 변경 가능한mutable object
아니냐고 ~
이랬다.
그런데 이번 챕터를 공부하고 나니 어디에 쓰는지 감이 온다.
useRef
는 DOM
을 조작하기 찰떡이다.다음과 같은 컴포넌트에서 focus
버튼을 누르면 input
태그가 focus
되도록 하고싶다고 해보자
focus
:input element
의 이벤트 종류로 마우스 커서가input
태그로 가는 이벤트
이렇게 직접 document.querySelector
를 이용해서 태그를 가져와서 focus
시켜버리면 되기는 한다.
다만 이는 몇 가지 문제가 존재한다.
React
에서 직접적으로 Actual DOM
에 접근하면 발생하는 단점Virtual DOM
을 조작하며 얻는 이점을 포기해야 한다.리액트는 근본적으로 Virtual DOM
들간의 reconciliation
과정을 통해 차이가 나는 노드를 찾아
해당 노드의 수정만 빠르게 하여 렌더링 성능을 최적화 한다는 장점이 있는데
Virtual DOM
을 건너 뛰고 바로 Actual DOM
에 접근하는 행위는 reconciliation
과정에서 예상치 못한 문제를 야기 할 수 있다.
컴포넌트 간의 계층적 구조로 이뤄진 Virtual DOM
을 조작하는 것이 아니고
실제 UI
에 렌더링 되는 Actual DOM
을 직접 조작 하는 것이기 때문에
Virtual DOM
만 신경 써야 하는게 아닌, Actual DOM
까지 함께 신경써야 한다.
이는 유지 보수를 더욱 어렵게 하고 확장성을 낮춘다.
1번에서 말했듯이 Virtual DOM
간의 reconciliation
으로 최대한 reflow , repaint
를 낮추고자 하는데
이러한 과정 없이 Actual DOM
을 조작하게 되면 reconciliation
과정으로 얻는 이점을 얻지 못해 성느이 저하될 수 있다.
Virtual DOM
을 조작하는 컴포넌트의 경우 Virtual DOM
을 이용하는 어떤 컴포넌트간의 조합이 쉽지만
Actual DOM
을 조작하는 컴포넌트의 경우 다른 컴포넌트와 조합하여 쓰기 위해선, 필연적으로 해당 Actual Node
가 존재해야 하기 떄문에
확장성이 매우 떨어진다는 문제가 있다.
웬만하면
Virtual DOM
조작 할 때는Virtual DOM
만 조작하자고 ~!~!
useRef
는 어떻게 Virtual DOM
의 노드를 선택 할까useRef
는 특히 Virtual DOM
의 노드를 선택하기 가장 안성맞춤이다.
컴포넌트의 반환값인 JSX
는 문자열이 아닌 React
의 객체이다. 해당 객체의 ref props
를
useRef
객체로 설정해준다면 마치 직접 node
를 설정한 것 처럼 사용 할 수 있다.
이처럼 useRef
에 Virtual DOM
의 Node
를 설정해주는 것은 useRef
를 사용하는
가장 흔한 예시라고 한다.
초기 렌더링 이전까지
ref.current
는node
를 가리키지 못한다.컴포넌트가 호출되면
rendering phase => reconciliation => commit phase
가 존재한다고 하였다.
Render Phase
(virtual DOM
을 생성하는 단계) 에서ref.current
는Actual DOM
을 가리키지 못한다.하지만
Commit Pahse
(Actual DOM
이 생성되는 단계) 에서 비로소 리액트는ref.current
에Virtual DOM
으로 인해 생성된Actual DOM
의 노드와ref.current
값을 바인딩 한다.
useRef callBack
useRef callback
은 JSX ref
값에 ref
객체를 넣어주는 것이 아니라
콜백함수를 넣어주어 해당 노드가 렌더링 될 때 마다
해당 노드를 인수로 받아 함수를 실행 할 수 있게 한다.
현재 introduce
콜백함수는 노드가 렌더링 되면 본인을 로그하도록 하는 콜백함수이다.
그런데 보면 ref
들이 모두 두 번씩 호출되는 모습을 볼 수 있다.
왜그럴까 ?
ref callback
은 Render Phase , Commit Phase
때 두 번 호출된다.키킥 제목 그대로다
컴포넌트가 새로 렌더링 되어 재호출되면 ref
에 있는 callback function
은
render phase , commit phase
때 모두 호출된다.
만약 Commit Phase
에서만 호출하고 싶다면 다른 훅이 있을지 모르겠지만
null
이 아닐때로 해야겠지 ?
ref
는 해당 노드를 찾지 못하면null
값을 반환한다.
useRef
를 컴포넌트에 넣어줄 수는 없을까 ?위에서 <input ref = {..}>
처럼 JSX
문법에서 ref
를 선언해주었으니
컴포넌트도 혹시 ref
를 이용해 변수처럼 다룰 수는 없을까 ?
해당 오류가 뜨며 안된다.
Components
에는 ref
를 줄 수 없다고 한다.
리액트는 의도적으로 이와 같은 행위를 금지시켜두었다.
useRef
는 escape hatch
역할을 한다고 했다. (React
외부의 것들에 접근 할 수 있게 해주는)
하지만 React
내부의 함수형, 클래스형 컴포넌트들에게도 useRef
를 이용하는 것은 역할에 맞지 않을 뿐더러
컴포넌트가 컴포넌트를 조종하는 행위들은 컴포넌트간 의존성을 높힐 뿐더러
여러 로직들이 조각 조각 흩뿌려지게 되기 때문에 금지해두었다.
그로 인해 리액트는 div , p , span , input ..
등과 같은 element
에만 ref
를 지정 할 수 있게 해두었다.
Ref
를 컴포넌트를 통해 건내줄 수는 있다.하지만 컴포넌트에서 ref
를 받아 해당 컴포넌트를 이루고 있는 element
들에게 건내주는 것은 가능하다.
fowardRef
를 이용하면 된다.
forwardRef
를 이용해 컴포넌트를 감싸주면 컴포넌트가 ref
를 인수로 받아
컴포넌트를 구성하는 엘리먼트들에게 ref
를 건내 줄 수 있게 해준다.
이 때 forwardRef
로 감싸지는 함수는 화살표 함수로 하는 것이 더 가독성 양상에서 좋기에
화살표 함수를 이용 할 수 있다.
In design systems, it is a common pattern for low-level components like buttons, inputs, and so on, to forward their refs to their DOM nodes. On the other hand, high-level components like forms, lists, or page sections usually won’t expose their DOM nodes to avoid accidental dependencies on the DOM structure.
다만 디자인 시스템에서 엘리먼트들에게 ref
를 건내주는 것은 자주 보이지만
상위 컴포넌트에의 ref
를 하위 컴포넌트에게 forwardRef
로 건내주는 것은 지양해야 한다고 한다.
그렇게 하면 상위 컴포넌트와 하위컴포넌트의 의존성이 강해지기 때문이다.
엘리먼트에서 ref
를 이용한 로직을 처리하고 싶다면
차라리 ref
를 인수로 받기보다, 컴포넌트 내에서 로직을 처리하는 것이 좋을 것 같다.
useImperativeHandle
는 접근을 제한하는 중개자역할 ref
를 이용하면 element
에 접근이 가능하기 때문에
ref
값으로 element
의 스타일을 변경하거나 식별자를 변경하는 등 어떤 행위가 가능하다.
이에 예기치 못한 행위를 방지하고, 정해진 행위만 할 수 있도록 useImperativeHandle
훅을 이용 할 수 있다.
우선 useImperativeHandle
을 이용하기 위해서는
컴포넌트 내의 localRef
객체를 생성해주어야 한다.
컴포넌트 내에서 생성한 localRef
객체는 엘리먼트의 ref
객체가 된다.
(부모 컴포넌트로부터 내려온 ref
객체를 할당해주지 않는다.)
useImperativeHandle
훅 은 두 가지 인수를 받는다.
ref
: 부모컴포넌트로부터 받은 ref
객체 createHandle
: 콜백함수이며 ref
객체가 할당된 엘리먼트에서 사용 가능한 메소드 , 프로퍼티를 담은 객체를 반환한다.useImperativeHandle
을 통해 컴포넌트 내에서 정의된 localRef
객체를 할당 받은 엘리먼트는
부모 컴포넌트에서 전달한 ref
객체를 통해 조작 할 수 있다.
하지만 createHandle
내에서 정의된 프로퍼티나 메소드만 접근 가능하다.
만약 그 외의 값에 접근하고자 한다면 접근 할 수 없기 때문에 undefined
가 된다.
정리
useImperativeHandle
은 부모 컴포넌트에서 내려준ref
와 컴포넌트 내에서 정의된localRef
를 이어주는 중개자 역할을 한다.중개자 역할이면서, 접근 가능한 값들을 제한해줌으로서 예기치 못한 현상을 방지한다.
Node
에 ref
를 언제 부착할까앞서 살펴보았듯이 노드에 ref
객체들을 부착하면 UI
가 업데이트 되는
render phase , commit phase
에서 모두 ref
가 부착되는 모습을 볼 수 있다.
render phase
에서는 Actual DOM
을 찾을 수 없기 때문에 ref
값이 null
을 가리키고
commit phase
이 되어서나 Actual DOM
을 찾을 수 있기에 실제 엘리먼트를 가리킬 수가 있다.
이렇게 render phase
에서의 ref
값이 null
로 호출됨에 의해서 예기치 못한 에러가 발생 할 수 있다.
이러한 방법은 ?.
과 같은 null 병합 연산자
를 이용하여 null
이 아닐 때에만 attribute
를 붙여주는 등의 방법을 이용하여 해결 할 수 있다.
혹은 Commit Phase
에서만 호출될 수 있도록 하는 useEffect
와 같은 훅을 이용 할 수 있는데 이는 다음 페이지에서 공부해보도록 하자
state
를 업데이트 하는 Flushsync
위 코드에서 button
을 클릭하면 stars
에 별이 하나 추가되어 상태가 업데이트 시키고
마지막 별에 식별자를 추가하고 삭제하는 행위를 통해 회전 시키고자 한다.
하지만 결과물을 보면 추가한 후의 마지막 별이 회전하는 것이 아니라
별을 추가 하기 전의 마지막 별이 회전하는 모습을 볼 수 있다.
이는 상태값의 변화가 렌더링 이후에 일어나도록 queued
되었기 때문이다.
setStars
직후에 state
가 업데이트 된다면 lastStar
는 상태가 업데이트 된 배열에서 골라지기 때문에
우리가 원하는 로직을 구현 할 수 있을 것이다.
FlushSync
react-DOM
에서 제공하는 FlushSync
는 상태값 변경을
렌더링 이후에 일어나도록 대기 시키는 것이 아닌, 동기적으로 변경 하도록 한다.
state
가 변경된 이후의 lastChild
를 고르게 하기에 원하는 로직을 구현 할 수 있다.
FlushSync
는 나중에 공식문서를 통해 더 봐봐야겠다.
react-DOM
에서flushSync
를import
해왔는데
[A5] 왜 나는 React를 사랑하는가 에서react , react-DOM
등과 같이 리액트가 파일들을 나눠둔 이유를 설명하는데 이것을 참고하면 좋을 것 같다.
useRef
를 이용 할 때 신경써야 할 부분useRef
는 Virtual DOM
에 ref
를 부착한 채 rendering -> commit
시키면
Actual DOM
이 생성되었을 때 해당 실제 엘리먼트와 Virtual DOM
에 존재하는 ref
가 부착된 JSX
객체를 바인딩 하도록 해준다.
ref
는 Actual DOM
을 조작 할 수 있게 해주지만 결국은
Virtual DOM
에 부착된 후 Actual DOM
에서 해당 엘리먼트를 찾아야 역할을 할 수 있다.
이는 Virtual DOM
과 Actual DOM
의 모습이 잘 맞아야 ref
를 사용 할 수 있다는 것이다.
다음과 같은 예시를 들어보자
다음과 같은 컴포넌트들을 만들고 각 버튼이 기능하는 컴포넌트들을 만들어보자
App.js
최상단 컴포넌트인 App
컴포넌트의 경우 Context
를 제공하며 하위 컴포넌트로 Stars , AllButtons
들이 존재한다.
ContextProvider.js
ContextProvider.js
에서는 3가지 Context
를 제공한다.
마지막 엘리먼트를 가리키는 lastStarRef
와 state , setterFunction
인 stars , setStars
.
각 컨텍스트에 접근 가능하도록 ContextProvider.js
전역 환경에 정의된 Context
에 접근하는 커스텀 훅인 useRefContext , useStarContext , useSetterContext
메소드도 export
해주었다.
Stars.js
상위 컨텍스트에서 정의된 state
인 stars
를 받아 렌더링 하는 컴포넌트이다.
./Buttons/AllButtons.js
AllButtons
컴포넌트는 AddButton , RotateButton , RemoveButtonV , RemoveButtonA
를
button-wrapper
식별자를 가진 태그로 감싼 컴포넌트이다.
./Buttons/AddButton.js
AddButton
은 stars
상태를 변경한다.
이 때 마지막 별에 ref
를 부착해주기 위해 마지막 별에 존재하는 ref
를 변경하여 업데이트 하도록 한다.
./Buttons/RotateButton.js
RotateButton
은 ref
객체가 붙어있는 Actual DOM
에 접근하여 식별자를 붙였다 뗌으로서 애니메이션 효과를 가져온다.
./Buttons/RemoveButtonV.js
RemoveButtonV
컴포넌트는 Virtual DOM
에서 상태를 변경하여 별을 제거하는 컴포넌트이다.
state
를 setter function
에 맞춰 삭제함으로서 ref
가 마지막 별에 잘 부착되도록 한다.
./Buttons/RemoveButtonA.js
RemoveButtonA
컴포넌트는 마지막 별을 가리키는 ref
객체를 받아
Actual DOM API
인 remove
로 Actual DOM
에서 해당 노드를 강제로 제거한다.
(Virtual DOM
의 변화를 야기하지는 않는다. 딱 봐도 문제를 일으키게 생겼다.)
Virtual DOM
과 Actual DOM
의 차이가 존재하지 않을 때Virtual DOM
을 변경하고 render
시킴으로서 Actual DOM
과의 차이가 존재하지 않을 때에는
모든 기능이 문제 없이 잘 작동하는 모습을 볼 수 있다.
Virtual DOM
과 Actual DOM
의 차이가 존재 할 때하지만 Actual DOM
의 API
인 remove
를 이용하여 노드를 제거하면
ref
가 업데이트 되지 않아 rotate last Star
도 작동하지 않고
Actual DOM
을 이용한 추가 삭제가 되지 않는 모습을 볼 수 있다.
하지만 이후
Actual DOM
의 API
를 이용 후 Virtual DOM
의 상태를 변경하려고 하니 오류가 뜬다.
왜 이런 일이 발생할까 ?
Actual DOM
에서 ref
가 참조하는 노드가 사라졌는데 왜 여전히 ref
는 그 노드를 참조할까 ?현재 상황을 보면 Virtual DOM
에서는 별이 3개가 존재하고 , lastStarRef
는 Actual DOM
에서 사라진 ⭐
를 참조하고 있었다.
그런데 이상하다. 그러면 rotate last Star
버튼을 누르면 Actual DOM
에 존재하지 않는 노드를 불러오니 오류가 나야 하는거 아닌가 ?
Actual DOM
에서 ref
가 참조하는 노드를 삭제한 후 회전 버튼을 눌러보자
이것도 아주 이상하다.
이런 일이 발생하는 이유를 시뮬레이션을 통해 살펴보자
현재 상황은 이렇다.
Actual DOM
에서 제거하여 Virtual DOM
과 Actual DOM
의 괴리가 발생 한 후
Add
-> Remove last Start (Virtual DOM)
-> Remove last Start (Virtual DOM)
(오류 발생)
일련의 과정에서 어떻게 리액트가 일을 처리하고 , ref
값이 변하는지 확인해보자
Initial Render
After Virtual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={3} ref = {ref}>⭐</div>
Actual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={3} ref = {ref}>⭐</div>
초기에 렌더링 되면 다음과 같은 모습일 것이다.
Actual DOM
에서 별 제거하기Actual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
// Actual DOM 에서 별이 제거되어도 Virtual DOM 은 상태가 업데이트 되지 않는다.
After Virtual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={3} ref = {ref}>⭐</div>
// 이 때 Virtual DOM 의 상태가 없데이트 되지 않았기 때문에
// 2번 단계에 있는 Virtual DOM 의 ref 는 1번 단계에 있는 Actual DOM 의 node 를 가리키고 있다.
이 때 부터 Virtual DOM
과 Actual DOM
의 괴리가 시작된다.
Add
로 별 추가하기Before Virtual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={3} ref = {ref}>⭐</div>
After Virtual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={3}>⭐</div>
<div key={4} ref = {ref}>⭐</div>
// Before Virtual DOM 과 After Virtual DOM 의 차이를 확인한다.
// 차이점 : key 가 4인 node 가 추가되었다.
// (Virtual DOM 이 update 되면서 Virtual DOM 의 ref 와 Actual DOM 의 ref 가 동기화된다.)
// Actual DOM 에 appendChild({.. key 가 4인 ⭐ 노드})
Actual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={4} ref = {ref}>⭐</div>
Remove (Virtual DOM)
로 별 제거하기Before Virtual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={3}>⭐</div>
<div key={4} ref = {ref}>⭐</div>
After Virtual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={3} ref = {ref}>⭐</div>
// Before Virtual DOM 과 After Virtual DOM 의 차이를 확인한다.
// 차이점 : key 가 4인 node 가 사라졌다.
// (이 떄 key 가 3인 노드에 ref 를 선언했으나 Actual DOM 에서 3인 노드를 찾을 수 없으니
// 현재 ref.current 의 값은 null 이다. )
// Actual DOM 에서 removeChild({.. key 가 4인 ⭐ 노드})
Actual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
Remove(Virtual DOM)
로 별 제거하기Before Virtual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
<div key={3} ref = {ref}>⭐</div>
After Virtual DOM
<div key={1}>⭐</div>
<div key={2} ref = {ref}>⭐</div>
// Before Virtual DOM 과 After Virtual DOM 의 차이를 확인한다.
// 차이점 : key 가 3인 node 가 사라졌다.
// Actual DOM 에서 removeChild({.. key 가 3인 ⭐ 노드})
Actual DOM
<div key={1}>⭐</div>
<div key={2}>⭐</div>
// Actual DOM 에 key 가 3인 노드가 없으니 에러 발생
이런 일이 벌어진거였다.
나는 사실 한시간 동안 이런 일이 왜 발생할까 ? 생각을 계속했는데
그렇게 멍청하게 이해 못한 이유가
Virtual DOM
과 Actual DOM
의 괴리가 시작되어도 상태가 업데이트 되면
1:1 로 대응이 되는줄 알았다.
사실은 그냥 rendering
을 기준으로 Virtual DOM
간의 차이가 나는 부분만 appendChild , removeChild
하는 것인데 말이다 !!
key
를 그려가면서 흘러가는 상황을 그려보니 이해가 드디어 되었다 휴
최근 유튜버인 양동준 님의 동영상 중 useRef
를 잘 이용한 프로젝트가 인상 깊었다는 사람의
합격 수기를 보았다.
그걸 보고 나서 useRef
를 더 열심히 공부해봐야지!! 하고 공부했는데
공부 하다보니 useRef
보다 Virtual DOM , Actual DOM
간의 괴리를 더 깊게 한 기분 ..
그래도 reconciliation
단계에 대한 이해를 드디어 제대로 한 것 같아서 기분이 좋다.