바닐라 자바스크립트로 DOM 컨트롤을 할 때는 getElementById
나 querySelector
등을 이용하여 DOM의 속성을 변경하거나 추가하거나 제거를 해왔습니다. 하지만 리액트에서는 조금 다릅니다. 물론 저 방법으로 할 수도 있지만 리액트에서는 동적으로 DOM을 컨트롤하기 위해 class component에서는 createRef를 그리고 Hooks에서는 useRef를 이용했습니다. 최근에 Typescript에서 useRef를 사용할 때 마주쳤던 수많은 오류(“Object is Possibly null” 이라던가…)를 해결하기 위해 해온 삽질들에 대해 적어 볼까 합니다.
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";
이렇게도 가능합니다.
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였습니다.