main

리액트 딥다이브 살펴보기 X

Log
12

이번 포스팅은 4-3장과 5-1장까지 내용을 정리한 포스팅입니다.

📌4-3장 내용 정리하기

예전에 Next.js에서 Css-in-Js관련된 라이브러리를 사용했을 때, 호환되지 않는 부분이 많았는데 Next.js의 문서화가 정리되면서 호환성 관련해서 많이 개선이 되었다는 것을 알게 되었어요.

npm install styled-components

styled-component를 설치하고, next.config.js에 아래와 같이 설정만 해주면 끝나더군요...!🥹

module.exports = {
  compiler: {
    styledComponents: true,
  },
}

이외에도 emotion이나 styled-jsx설정 관련해서 Next.js 공식문서에 아주 잘 정리되어있는 것을 확인할 수 있었습니다.

Next.js 공식문서

사실 저는 4-3장에서 왜 Next.js에서 Css-in-Js가 제대로 호환되지 않는 이유에 대해서 자세하게 기술되어있을 줄 알았는데, 아래 문장으로 끝나있더군요 🥲

스타일이 브라우저에 뒤늦게 추가되어 FOUC(flash of unstyled content)라는, 스타일이 입혀지지 않은 날것의 HTML을 잠시동안 사용자에게 노출하게 된다.

그래서 Next.js에서 app라우터를 사용한다는 가정에, Css-in-Js 설정을 제대로 해주지 않으면 대체로 use client 디렉티브를 사용해야하더라구요.. Next.js에서 framer-motion을 사용할 때, 대체로 모두 use client 디렉티브를 사용해야했던...😅 그래서 반응이 매우 다양해야하는 페이지를 사용할거라면 Next.js사용은 비추하는 듯 했습니다.
아무래도 SSR같은 경우에는 처음에 로드되는 HTML은 입혀지지 않은 상태로 보여지고 이후 JS번들이 hydrate되면서 반응형이나 스타일이 뒤늦게 적용되기 때문인 것 같았어요.

  • 결론
    • Next.js에서 너무 반응이 다양해야하는 페이지는 적절하지 못하다.
    • css-in-js를 사용한다면, 설정을 제대로 해주지 않으면 스타일이 적용되지 않은 HTML이 잠깐 보일 수 있다.


📌5-1장 내용 정리하기

5-1장이 책 내용이 직접 상태라이브러리를 구현해보는 내용이었는데요, 책을 읽으면서 tearing현상과 useSyncExternalStore 훅을 왜쓰는지에 대한 이해를 하고자 내용을 정리해보았습니다.

많은 블로그에서 tearing현상이 아래와 같은 현상이라고 많이 적혀있었습니다.
240116-095505

많은 블로그에서 tearing관련된 설명 이미지를 아래를 첨부하고 있었는데요, 실질적으로 저는 이 tearing 현상을 직접적으로 보고 싶었습니다.
240116-100014

그럼 이 tearing현상이 왜 일어나고 어떤 현상일까요?

📌 externalStore, internalStore

리액트 관련 관계자들은 externalStoreinternalStore에 대해서 분류를 나눠서 설명하고 있습니다.

  • externalStore: mobx, redux, recoil, jotai, xtsate, zustand, rect query와 같은 외부 상태라이브러리들
  • internalStore: useState, useReducer, context, props와 같은 리액트에서 제공하는 상태관리 도구

tearing현상은 externalStore, 즉 외부상태라이브러리를 사용할 때, 주로 일어나는 현상이라고 합니다.

internalStore같은 경우에는 리액트에서 만든 관리도구다보니, fiber아키텍처가 적용된 이후로 내부적으로 상태관련해서 렌더링의 우선순위나, 스케줄링 등 관련 깊은 알고리즘이 잘 구현되어있다고해요. 이 알고리즘 관련된 코드는 사용자들이 볼 수 없으며 관리도구 API를 사용할 수 있기만 할뿐이라고 합니다.(useReducer, useState...)

반대로 externalStore는 리액트가 만든 라이브러리가 아니다보니, 렌더링 관련해서 문제가 생길 수 있다는 점이 존재했다고 해요. 그게 바로 tearing 현상입니다.



📌 tearing 현상 이해하기

아까 위에서 업로드 된 이미지를 기반으로 먼저 설명을 해보자면, 렌더링 도중 들어오는 유저 인터렉션에 대해 기존 렌더링을 중지하고 인터렉션에 대한 UI를 먼저 렌더링 하기 때문에 아래처럼 빨간색으로 렌더링 트리를 업데이트하는 도중 파란색의 인터렉션으로 인해 트리의 일부분이 파란색으로 렌더링될 수 있습니다. 이런 현상을 tearing이라고 하는데요,
240116-100014

쉽게 말하면 유저 인터렉션으로 인해서 기존 렌더링이 중지되었다가, 다시 돌아와 렌더링을 진행하는데, 이전 렌더링과 이후 렌더링이 일치하지 않는 현상이라고 이해하면 될 것 같습니다.

그래도 저는 이론적으로 설명이 조금 애매모호 하다고 느낀 것 같아요. 그래서 tearing현상을 직접 구현한 블로그와 코드가 있어서 같이 업로드 해봅니다.

기본적으로 리액트에서 외부 시스템과 동기화할 때 useEffect라는 훅을 사용한다고 해요. 따라서 컴포넌트의 렌더링 이후 화면이 업데이트 된 후에 발생하게 됩니다.

이런 특성을 사용한다면 리액트 외부에 있는 스토어와 동기화하는데 사용할 수 있게 됩니다.
Redux와 비슷한 방식으로 스토어를 만들어서 useEffect를 사용해 외부 시스템과 동기화를 구현해보겠습니다.

아래 예제에서는 Storecount 변수를 setInterval를 사용하여 1초마다 1씩 증가하도록 dispatch하고 이를 구독하는 Counter 컴포넌트를 만든 것입니다.

const store = {
  state: { count: 0 },
  listeners: new Set<() => void>(),
  // subscribe의 callback함수는 state 내부에서 구독할 값을 필터링 거는 함수입니다.
  subscribe: (callback: () => void) => {
    store.listeners.add(callback);
    return () => {
      store.listeners.delete(callback);
    };
  },
};
 
export const dispatch = (action: { type: string }) => {
  if (action.type === "increment") {
    store.state = { count: store.state.count + 1 };
  }
  
  store.listeners.forEach((listener) => listener());
};
 
export const useStore = () => {
  const [state, setState] = useState(store.state);
  useEffect(() => {
    const handleChange = () => setState(store.state);
    const unsubscribe = store.subscribe(handleChange);
    return unsubscribe;
  }, []);
 
  return state;
};
 

전체 코드는 아래 링크에서 확인하실 수 있습니다.
Fullcode

240116-142635
실제 코드를 실행시켜보면 사진처럼 버튼을 누르면 위와 같이 렌더링 중간에 스토어의 값이 바뀌었기 때문에 UI에 상태가 다르게 표시되는 Tearing 현상이 발생하는 것을 확인할 수 있습니다.

240116-142706

tearing현상에 대해서 더 자세한 글은 아래에서 확인하실 수 있습니다.
리액트에서 외부 시스템과 동기화하기


📌 externalStore에서 tearing 현상이?

그래서 externalStore을 사용하면 이런 tearing현상이 존재했다고 합니다. 그래서 redux의 메인테이너 개발자분께서 tearing 현상을 막기위해 리액트 팀이 만든 useMutableStore 훅을 사용 시 selector 함수를 useCallback으로 감싸줘야 하는 필요성에 대해 이야기를 했다고 합니다.
근데 selector함수에 매번 useCallback을 감싸야하는 건 보일러플레이트뿐만 아니라 넘 귀찮은 일이죠🥲

이전에 부트캠프에 참여했을 때, 리덕스를 배웠었는데 그때 selector함수를 매번 useCallback으로 감싸야하냐는 질문을 했었는데, 그러면 좋겠죠...?라는 말을 들었었는데... 그 이유가 tearing현상 때문이었다는 것을 오늘에서야 이해하게 되었네요.😅

하지만 redux는 v8이후부터는 패치가 적용되어서 useCallback을 사용하지 않아도 된다고 어디서 본 것 같은데, 확실하지는 않지만 패치되었다고 알고 있습니다. tearing현상을 막기 위해 패치가 적용되면서 사용한 훅이 useSyncExternalStore라고 합니다. 실제로 외부 상태 라이브러리를 직접 구현했을 때, tearing현상을 막기 위해 useSyncExternalStore 훅을 이용하면 보일러플레이트를 크게 줄일 수 있다고 해요.


📌 startTransition사용하면 tearing 현상을 막아주는 useSyncExternalStore

2021년도에 Daishi Kato가 리액트컨퍼런스2021년에 설명해놓은 영상인데요, 이영상을 보면 훅의 사용법을 한번에 이해할 수 있습니다...!
React 18 for External Store Libraries

Reference Doc

useSyncExternalStore 어후 이름이 너무 길어.
React - useSyncExternalStore hook
Concurrent React
리액트에서 외부 시스템과 동기화하기
React 18 useSyncExternalStore를 이용하여 전역 상태 구현하기 ex) recoil, zustand, jotai


김다은 이모지
Daeun Kim
Junior Frontend Engineer