반응형

React에는 굉장히 수많은 Hook이 있다. 지금도 계속 생기고 있을 것이다.

하지만 나의 경우에는 맨날 쓰는 Hook만 쓰긴 한다. useState  useEffect 정도… 사실 이것만 있어도 대부분의 기능을 구현하는데 문제 없었기에 그런 것도 있긴 하다만 React의 기능을 완전히 이용하지는 못하고 있었다.

오늘 회사에서 3분기 회고를 하는데 동료 분께서 이런 세태에 대해 언급하시면서 useLayoutEffect Hook이란게 있다라는 말씀을 하셨다. 뭔지는 몰랐지만 뭔지 궁금해져서 이번주에는 이에 대해 간단히 알아보도록 하자.

useEffect와 문제점

React Hook은 Functional Component에서도 기존 Class Component만이 제공하던 기능을 사용할 수 있도록 해준 기능이다.

그 중 핵심적인 기능이 상태 값 관리와 컴포넌트 생애 주기를 관리할 수 있다는 점이었는데, useEffect 는 그 중에서 생애주기에 집중하는 Hook이다.

기존의 Class Component에서 가지고 있던 몇 가지 메서드를 대체하는 역할을 한다. componentDidMount , componentDidUpdate , 그리고 componentWillUnmount 다. 컴포넌트의 Mount, 컴포넌트의 Update, 그리고 Unmount. 컴포넌트 생애 주기의 전부다.

기본적으로 useEffect 는 랜더링이 완료된 후에 동작한다. 이 랜더링에는 리랜더링도 포함된다. 혹은 특정 상태의 업데이트 시에만 구동되도록 할 수 있다.

과거에 썼던 글에서 사용한 코드 조각을 슬쩍 들고 왔다. 이걸로 useEffect 에 대해 살짝만 알아보자. 해당 글에서 useState  useEffect 에 대해 좀 더 자세히 알아볼 수 있다.

const App = () => {
  const [name, setName] = useState('');  useEffect(() => { // (1)
    console.log('render');
  });  useEffect(() => { // (2)
    setName('희찬');    return (() => { // (3)
      console.log('Cleanup'); 
    }
  }, []);  useEffect(() => { // (4)
    console.log(name);
  }, [name]);  return (
    <div>{name}</div>
  );
}

(1) useEffect 는 첫 랜더링과 매 리랜더링마다 동작할 것이다.

(2) 이 useEffect 는 Dependency Array가 [] 로 설정되어있다. 즉 비어있다. 이 경우에는 첫 랜더링에서만 동작할 것이다. 즉 componentDidMount 의 역할을 한다.

(3) return 안에 있는 메서드를 cleanup function이라고 한다. 이는 리랜더링 될 때 앞에 있는 callback을 실행하기 전, 혹은 Unmount할 때 동작한다. 정리하는 역할을 한다고 해서 cleanup이라고 하나보다. 이 경우에는 첫 랜더링에만 동작하는 useEffect 였으므로, 이 컴포넌트가 아예 Unmount될 때 동작할 것이다. 따라서 componentWillUnmount 의 역할을 할 수 있다.

(4) 이 경우에는 Deps에 [name] 이 들어있으므로, 리랜더링될 때 해당 Array 내의 값이 변경되어있으면 해당 메서드가 발생한다. 조건부 useEffect 라고 볼 수 있겠다.

 

그런데 useEffect 를 사용할 때 UI 입장에서는 문제점이 있다. useEffect  랜더링 후에 동작한다는 점이다.

문서를 보면 레이아웃 구성과 페인팅이 완료된 후에 발생한다고 한다. 이때 이벤트 리스너를 만들거나 비동기 작업을 시작하는 것은 적절하다.

하지만 만약 UI에 영향이 가는 작업이라면 어떨까? 일단 리랜더링이 다 된 이후에 useEffect 가 발동하면 초기화된 값과 우리가 원하는 값 사이의 괴리가 생길 수 있다.

function App() {
  const [name, setName] = useState<string>('');

  useEffect(() => {
    setName('heechan');
  }, []);
  
  return (
    <>
      <p>{`안녕하세요! ${name}`}</p>
    </>
  );
}

대충 이런 코드라고 치자.

랜더링 되자마자는 화면에 안녕하세요! 만 보이다, 찰나의 순간에 useEffect 발동 후 리랜더링되어 안녕하세요! heechan 이 된다. 그 번쩍이는 순간이 문제가 될 수 있다. (사실 보여주고 싶기도 했는데 생각보다 너무 랜더링이 빨리 돼서 눈에 안보인다… 더 복잡한 코드에서는 충분히 딜레이가 발생할 수 있다)

이런 사소한 문제들이 유저의 사용성을 해칠 수 있어서, 이런 문제를 해결할 방법이 필요하다.

useLayoutEffect

그래서 useLayoutEffect 가 등장했다. 위 캡처에서도 나와있듯 사용자에게 직접 UI로 보여야 하는 몇몇 요소의 경우에는 랜더링 이후로 지연할 수 없다. 번쩍거리는걸 보여주기보다는 그 이전에 동기화해야 한다.

이럴 때 useLayoutEffect 를 사용하면 된다. 사용법은 useEffect 와 완전 동일하다. useLayoutEffect(callback, [...]) 으로 쓰면 된다.

useLayoutEffect  레이아웃 구성과 페인팅 사이에 동기적으로 동작한다. useEffect 가 랜더링 후에 호출되는 것과 대조되는 특징이다.

이 이미지를 보면 그 순서를 확실히 알 수 있다. 업데이트 시 DOM 업데이트가 진행된 후에 useLayoutEffect 에 있는걸 실행한다. 그 후 브라우저에서 페인팅을 마치면 그 후에서야 useEffect 에 있는 callback을 실행한다.

이렇게 되면 브라우저가 페인팅, 즉 유저에게 보이기 전에 우리가 필요한 작업을 하기 때문에 번쩍이는 현상 없이 잘 보이게 된다.

그러면 아까 위에 적었던 코드도 이렇게 바꾸면 문제 없이 동작한다는 것이다.

function App() {
  const [name, setName] = useState<string>('');

  useLayoutEffect(() => {
    setName('heechan');
  }, []);
  
  return (
    <>
      <p>{`안녕하세요! ${name}`}</p>
    </>
  );
}
 

근데 위의 코드를 다시 유심히 살펴보자. 난 처음에 이 코드를 보고 이게 왜 동작하는건지 상당한 의구심이 생겼다.

분명히 useState 가 제공하는 setter의 경우 (위에서는 setName ), 컴포넌트의 리랜더링을 유도하는 것으로 알고 있는데, LayoutEffect가 랜더링 과정 중에 돌아간다고 해서 바로 바뀌는건 아니지 않나? 싶었다.

나의 상상 속에서는 DOM 업데이트 -> Layout Effect 실행 -> 여기 setName 이 있네? 나중에 리랜더링이 필요해! -> 페인팅 -> 리랜더링 -> 우리가 원하는 값으로 페인팅 이 과정이었다.

그래서 궁금해서 찾아보니 React 콜라보레이터 한 분이 남긴 Github Issue 댓글을 확인할 수 있었다.

https://github.com/facebook/react/issues/17334#issuecomment-553984285

LayoutEffect가 리랜더링을 필요로 할경우, 이 리랜더링 과정도 동기적으로 진행되며 브라우저의 페인팅이 지연된다는 말이었다.

그래서 useLayoutEffect 내에서 useState 의 setter를 사용하더라도 리랜더링이 나중에 진행되는게 아니라 바로 동기적으로 해야 할 리랜더링 다 진행 후 페인팅하기 때문에 번쩍거림, 쓸데 없는 페인팅에 대한 걱정을 덜 수 있다.

 

또 한 가지 궁금해진 점인데, 과거에 useMemo 도 랜더링 타임에 진행된다고 한 적이 있다. 그러면 useMemo , useLayoutEffect 중에 뭐가 먼저 동작할까?

 

이렇게 해서 첫 랜더링 때 flag 값을 바꾸면, 그 이후 무엇이 주르륵 실행될지 확인해보았다.

이 순서대로 동작했다. 혹시 순서 때문에 그런가 싶어 순서도 바꿔보았는데 그대로 이렇게 나온다.

아마 useMemo 가 호출되는 타이밍은, DOM 랜더링 과정 중이 아닐까 싶다. (근거를 못찾아서 확신은 없음) useLayoutEffect 는 그 이후, 페인팅이 되기 직전에 불리고, useEffect 는 페인팅까지 다 되고 나서 불리니까 저 순서가 맞지 않을까 싶다.

언제 무엇을 써야 하는가

위에서 계속 언급한 컴포넌트가 번쩍 거릴 수 있는 문제를 해결할 때 useLayoutEffect 를 사용하는 것이 도움이 될 것이다.

다만 최대한 useEffect 만으로 할 수 있는 방법을 고안해보고 사용하라고 한다. 기본적으로 useEffect 가 랜더링 후에 동작하는 이유는 랜더링 과정을 방해하지 않게, 오래걸리지 않게 하기 위해서다. useLayoutEffect 를 너무 남용하면 그런 의미를 퇴색시킬 수 있으니 꼭 필요할 때만 쓰라는, 그런 의미가 아닐까 한다.

 

React 18부터는 useEffect 의 호출 타이밍도 경우에 따라 달라질 수 있다고 한다.

뭔가 업데이트하는 로직이 flushSync 문 안에 들어있거나, 아니면 Discrete Event가 발생했을 때 useEffect 의 callback이 랜더링 전에 동기적으로 불린다는 내용이다.

useLayoutEffect 와는 다른게, useLayoutEffect 는 내부에서 발생하는 업데이트까지 전부 수행한 후 페인팅하지만, 이 useEffect 는 그냥 useEffect 내부에 있는 callback만 빠르게 호출하는거지 그 내부에서 발생하는 업데이트 요청은 페인팅 이후에 진행된다. 따라서 약간 용법이 다르다고 볼 수 있겠다.

이 새로운 useEffect  UI 업데이트보다는, 그 전에 불린 callback이 끝났는지 확인해야 할 때 사용하도록 유도되는 것으로 보인다.

이걸 보면 유저가 제출 버튼을 따닥 눌러서 여러번 실행될 수 있을 때, 그 전 작업이 종료되었는지를 확인해야 할 때, 동기적으로 동작하도록 유도할 수 있다.

사실 Discrete Event에 대해서는 잘 모르긴 하지만, 한 이벤트가 다음 이벤트에 영향을 줄 수 있는 이벤트를 의미한다고 한다. Click, Press 이런 이벤트들이 있다고 한다.

결론

사실 프레임워크보다는 웹 표준적인 근본 공부를 좀 더 해야 하긴 하는데…

그래도 useLayoutEffect 는 굉장히 흥미롭다. 적절히 잘 쓰면 우리 회사 프로덕트에도 큰 도움이 되지 않을까 싶기도 하다.

참고한 것

 

반응형

+ Recent posts