React18 과 SSR, batch update

devAnderson·2022년 2월 10일
0

TIL

목록 보기
54/103

들어가기 전

리엑트에서 SSR을 한다고 생각하면 이런 방향으로 진행될 것이다 (next.js를 다시 떠올리며)

  1. 서버사이드에서 필요한 데이터를 fetching한다
  2. 서버사이드에서 처리한 html을 response로 전달한다.
  3. 클라이언트 사이드에서 브라우저는 html의 스크립트 태그를 파싱한 후 자바스크립트 코드를 요청한다
  4. JS 엔진이 해당 내용을 파싱한 후, DOM에 js코드를 연결시킨다 (hydration)

이 과정 전체가 asynch하게 진행되기 때문에, 한 과정이라도 느려진다면 전체 앱이 띄워지기까지의 시간이 오래 걸릴 수밖에 없다. ( 예를들어, 서버에서 데이터 준비를 위해 패칭을 하는 api 과정이 네트워크 혼잡으로 느려질 수도 있다)

또한 브라우저 입장에서도 JS파일을 패칭하고, 로드한 뒤 이것을 DOM과 결합하는 Hydration 과정이 늦어진다면 UI는 존재하나 기능을 사용하지 못하는 (혹은 경우에 따라 에러를 발생시키는) 상황이 벌어질 수 있다.

즉, 모던 웹사이트에는 해당 딜레마를 어떻게 해결하는지가 중요한 과제이다

interaction blocking을 중점으로 할 것인가 vs FTP(first time to paint) 를 중요시 할 것인가

기존 리엑트 방식

기존 리엑트는 전자의 방식을 택하고 있었다. 즉, renderToString을 사용하여 서버로부터 전통적으로 html 전체를 가져오는 방식이었다.

html을 받아서 파싱하고 => JS를 받아서 파싱한뒤 => hydration이 될 떄까지 초기 페이지가 나오는 것을 기다리게 한다

이로 인해 사용자의 컴퓨터가 느릴 경우 초기 화면이 하얗게만 보이는 단점이 존재했다.

하지만, 새로운 버전으로부터는 pipeToNodeWritable을 이용하여 html을 청크단위로 받아올 수 있게 되었다. 이를 통해 FTB적으로 빨라진 결과를 가져올 수 있었다

이때 사용하게 되는 것은 라는 컴포넌트로, 이 아래에 감싸진 자식은 자동적으로 기존 React.lazy를 통해 코드 스플리팅으로 데이터를 받아오던 방식과 같은 방식을 취하게 된다

<Layout>
  <NavBar />
  <Sidebar />
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

이때 덜 받아져왔을 경우 로딩 스피너가 fallback으로 보인다.

즉, 빌드하는 시점에서 해당 파일은 이전 Next.js에서 봤던것처럼 웹팩을 통해 분리되어 따로 관리되고, 만약 해당 페이지의 요청이 필요할 때 이와 관련된 js 코드를 새롭게 받는 형태로 구성되어 있다. 즉, 이 JS가 네트워크적으로 받아지는 응답을 기다리는 것을 fallback으로 UI에 표현할 수 있는것이다.

해당 방식의 장점

만약 해당 JS가 받아와져서 Hydration까지 완료되는데 시간이 오래걸려 fallback을 계속 보여주는 상황이라도,
다른 컴포넌트들은 hydration이 완료되었기 떄문에 사용자가 이 완료된 컴포넌트를 이용해 소통할 수 있게 한다.

또한 React18의 suspense는 일반적으로 DOM에 기록된 순서대로 hydrating을 동기적으로 진행하는데, 만약
한 컴포넌트 내에 두가지의 hydrating이 진행되고 있을 때 사용자가 후위의 컴포넌트와 인터엑션을 하기를 원하여 클릭을 했을 경우, hydrating의 우선순위를 조절하여 해당 컴포넌트를 먼저 로드하는 방향으로 진화하였다.

<Layout>
  <NavBar />
  <Suspense fallback={<Spinner />}>
    <Sidebar />
  </Suspense>
  <RightPane>
    <Post />
    <Suspense fallback={<Spinner />}>
      <Comments />
    </Suspense>
  </RightPane>
</Layout>

위처럼 두개의 suspense가 존재할 때, 사용자가 component 영역을 클릭하게 된다면

이것을 selective hydration이라고 부르고 있다.

batch update

이건, 정말 놀라운 개념이었다. 리엑트는 기본적으로 배칭 업데이트를 치원한다

배칭 업데이트란, 예를들어 상태의 변화값이 일어나는 함수의 호출에 대해서 여러번 리랜더링 하는 것이 아니라, 모든 상태변화 함수의 호출을 완료한 후 단 한번의 리랜더링을 한다는 개념이다

function handleClick() {
    setCount(c => c + 1); // Does not re-render yet
    setFlag(f => !f); // Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }

예를들어 위와같은 코드를 짰다고 하자.
나는 어제까지만 하더라도 이렇게 상태를 여러번 업데이트하는 함수가 호출되면 리랜더링을 여러번 할 것이라고 생각했기에 어떻게든 상태를 하나로 두고 관리해야한다는 강박관념을 가지고 있었다.

하지만, 리엑트는 상태업데이트 함수가 여러번 호출된다면 이 모든 결과값을 다 계산한 뒤 단 한번의 리 랜더링을 한다.
놀랍지 않은가.

다만 react 18버전 전까지는 이런 상태 업데이트가 브라우저 이벤트 환경에서만 동작하고 Promise를 이용한 비동기 작업의 마이크로 테스크 큐에서는 적용이 되지 않았다. (즉, 기존까지의 배치 업데이트는 해당 작업이 동기적으로 처리되고 있었음을 나타낸다)

하지만 새로운 버전부터는 첫 root element를 createRoot 함수를 통해 렌더링할 경우 비동기 작업 내에서의 상태 업데이트도 배치 업데이트를 하도록 만들 수 있다.

// onClick 핸들러
function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}

// setTimeout
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}, 1000);

// fetch API
fetch(/*...*/).then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
})

// addEventListener callback
elm.addEventListener('click', () => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
});
profile
자라나라 프론트엔드 개발새싹!

0개의 댓글