[TS]useRef 자세히 알아보기(Typescript, useLayoutEffect)
서론
바닐라 자바스크립트로 DOM 컨트롤을 할 때는 getElementById나 querySelector 등을 이용하여 DOM의 속성을 변경하거나 추가하거나 제거를 해왔습니다. 하지만 리액트에서는 조금 다릅니다. 물론 저 방법으로 할 수도 있지만 리액트에서는 동적으로 DOM을 컨트롤하기 위해 class component에서는 createRef를 그리고 Hooks에서는 useRef를 이용했습니다. 최근에 Typescript에서 useRef를 사용할 때 마주쳤던 수많은 오류(“Object is Possibly null” 이라던가…)를 해결하기 위해 해온 삽질들에 대해 적어 볼까 합니다.
useRef의 용도
useRef는 가장 일반적으로는 JSX 혹은 TSX 내 마크업 요소의 레퍼런스를 가져오기 위해 사용했습니다. 하지만 이번엔 그 전의 useRef의 특징을 알아보기 위해 plain useref 코드를 작성해보겠습니다.
import React, { useRef, useState, useEffect } from 'react';function App() {
const [count, setCount] = useState<number>(0);
const refCount = useRef<number>(0); // 0 값이 refCount.current에 저장됩니다.
useEffect(() => {
console.log("count state : ", count)
console.log("ref count state : ", refCount)
})
return (
<>
{count}
<button onClick={() => {
setCount(count => count + 1)
}}>increment</button>
<button onClick={() => {
refCount.current++
}}>ref increment</button>
<button onClick={() => {
console.log('current ref is...', refCount)
}}>show Ref</button>
</>
);
}export default App;
위 코드로 useState와 useRef의 차이점을 알 수 있습니다. useState로 지정된 count의 상태를 변화시키기 위해 첫 번째 버튼을 클릭하면 state가 계속 변화되기 때문에 콘솔에 useEffect 내의 console.log가 계속 해서 실행되며 count와 refCount를 보여줍니다. 이 부분은 당연합니다.
하지만 두 번째 버튼을 눌러서 refCount를 늘리면 컴포넌트가 re-render되지 않습니다. state나 props의 변화가 없었기 때문에 어찌보면 당연하지만 이게 useRef의 속성입니다.
그리고 refCount값을 확인하기 위해 세 번째 버튼을 누르면 refCount 내의 숫자가 current라는 변수에 담겨 늘어나 있는 것을 확인 할 수 있습니다.
useRef로 DOM 컨트롤 하기
이번엔 useRef로 DOM의 레퍼런스를 받아와 보겠습니다.
먼저 간단한 css파일을 작성해줍니다.
.circle{
width: 300px;
height:300px;
background-color: #000;
border-radius:50%;
transition:opacity 1s linear;
}
그 다음 App.tsx 파일을 아래와 같이 작성해줍니다.
import React, { useRef, useEffect } from 'react';
import styles from "./App.module.css"function App() {
const ballRef = useRef<HTMLDivElement>(null) useEffect(() => {
const { current } = ballRef;
if (current !== null) {
current.style.opacity = "0";
} return clearTimeout()
})return (
<>
<div ref={ballRef} className={styles.circle}></div>
</>
);
}export default App;
ballRef에 <HTMLDivElement>를 제네릭 타입으로 설정을 해줍니다. 이러한 타입은 VSCode를 사용 중이라면
<div ref={ballRef} className={styles.circle}></div>
의 ref에 마우스를 올리면 VSCode가 타입을 알려줍니다.
그리고 useRef로 생성된 객체 내에는 current라는 게 들어있는데 이 current에값이 저장되는 것을 위 state와 ref의 비교 코드에서 확인할 수 있었습니다.
ref로 마크업 요소의 레퍼런스를 가져오면 current내에 이 레퍼런스가 저장되게 됩니다.
const { current } = ballRef;
따라서 이렇게 current을 읽어오고 사용을 하는데 이 때 무조건 null check를 해주셔야 합니다.
if (current !== null) {
current.style.opacity = "0";
}
또는 옵셔널 체이닝을 사용하여
current?.style.opacity = "0";
이렇게도 가능합니다.
useLayoutEffect
useEffect는 알겠는데 useLayoutEffect는 무엇일까요? useLayoutEffect는 React Hooks의 하나의 라이프사이클 단계중 하나라고 할 수 있습니다.
import React, { useEffect, useLayoutEffect } from 'react';function App() {
useLayoutEffect(() => {
console.log('useLayoutEffect')
})
useEffect(() => {
console.log('useEffect')
})
console.log('render')
return (
<>
</>
);
}export default App;
위와 같이 코드를 작성하고 콘솔을 확인하면
위와 같은 순서로 로그가 출력됩니다.
즉 useLayoutEffect는 순서적으로는 렌더와 useEffect 사이에 있습니다. 하지만 useEffect와 useLayoutEffect는 거의 동일하게 작동을 하게 되는데 가장 큰 차이점은 바로 동기적으로 실행된다는 점입니다.
useEffect의 경우 useState의 초기 값을 0으로 둔 count라는 변수가 있다고 했을 때
useEffect를 통해 count를 10으로 setState한다면 처음에 아주 잠깐 0이 었다가 바로 10이 됩니다.
물론 useEffect내에 단순하게 디펜던시 없이 setState를 해서는 안됩니다!
하지만 useLayoutEffect에서 setState를 한다면 이 변화를 감지하고 동기적으로 이 변화를 적용시킨 후 렌더링합니다. 따라서 state가 10인채로 화면에 보여진다는 의미죠.
아래 예제를 보면 더 쉽게 이해가 가실겁니다.
import React, { useState, useEffect, useLayoutEffect } from 'react';function App() {
const [number, setNumber] = useState(0);useEffect(() => {
if (number === 0) {
setNumber(10)
}
}, [number])return (
<>
{number}
<button onClick={() => {
setNumber(0)
}}>Button</button>
</>
);
}export default App;
위 코드는 버튼을 누르면 number가 0이 되고 useEffect를 통해 state가 0이 됨을 감지하였을 때 다시 number를 10으로 올려주는 코드입니다.
위처럼 작성하고 실행해서 버튼을 누르면 {number} 부분이 계속 0과 10을 왔다 갔다 하느라 깜빡거리는 것을 볼 수 있습니다.
이는 useEffect가 비동기적으로 작동하기 때문인데요. 이 경우 useEffect를 useLayoutEffect로 바꿔서 실행하면 숫자가 깜빡거리지 않고 10을 계속 유지하는 것을 볼 수 있습니다. 즉 useEffect는 일단 화면을 보여주고 변화를 주는 반면에 useLayoutEffect는 변화를 적용 시킨 후 화면을 보여줍니다.
그래서
이거랑 useRef랑 무슨 연관이 있을까요? useRef는 DOM을 조작하는데 쓰입니다. 애니메이션을 추가하기도 하고 css 스타일을 얹기도 하고 자바스크립트 함수를 통해 focus와 같은 내장 메소드를 사용하거나 또는 이벤트 리스너를 추가할 수도 있죠.
useLayoutEffect는 변화된 DOM을 브라우저가 렌더할때 동기적으로 변화가 완료된 DOM을 보여주게 됩니다. 즉 직접적으로 DOM 자체를 조작할 때 사용할 수 있도록 최적화(Optimization)되어 있는 Hook이라고 할 수 있습니다.
대부분의 경우에서 모든 DOM의 변화가 있는 코드 작성에서는 useEffect 대신 useLayoutEffect를 사용하는 것이 맞습니다.
결론
최근 포트폴리오에 들어갈 토이 프로젝트를 제작 중에 있는데 useRef를 사용할 부분에서 몇 가지 오류와 내 생각대로 작동하지 않는 코드를 보며 그냥 useRef 다시 한 번 공부하자는 마음으로 구글을 떠돌다가 useLayoutEffect까지 알게 되었습니다. useLayoutEffect는 어느정도 리액트에 대해서는 잘 알지는 못해도 못들어 본건 없지 않을까? 하는 저에게 다시금 더 공부를 해야겠다는 동기를 부여해준 Hooks였습니다.