[번역] 세 가지 아키텍처에서의 리액트 Suspense

강엽이·2023년 9월 25일
26
post-thumbnail

원문 : https://elanmed.dev/blog/suspense-in-different-architectures

리액트 Suspense는 이상한 여정을 걸어왔습니다. 리액트 Suspense는 수년 동안 거의 사용되지 않았으며 로딩 상태를 렌더링하는 멋진 방법일 뿐 별다른 이점이 없는 것으로 여겨졌습니다. 하지만 최근 출시된 리액트 18에서 Suspense는 다시 살펴볼 가치가 있는 완전히 새로운 이점을 제공합니다. 안타깝게도 이러한 장점은 그다지 매력적이지 않은 것부터 난해한 것까지 다양하며 앱의 아키텍처에 따라 크게 달라질 수 있습니다. 최근 가장 일반적인 세 가지 렌더링 아키텍처와 리액트 Suspense가 어떤 역할을 할 수 있는지 살펴봅시다.

tl;dr

  • 클라이언트 사이드 렌더링: React.lazy가 로드되는 동안 폴백을 표시합니다. Suspense 호환 프레임워크로 데이터를 불러올 때 로딩/오류를 선언적으로 처리합니다.
  • 서버 사이드 렌더링: 위의 모든 내용 + <Suspense />로 감싸진 서버 사이드 렌더링 컴포넌트는 클라이언트에서 선택적으로 하이드레이션됩니다.
  • 서버 컴포넌트: 위의 모든 내용 + <Suspense />로 감싸진 비동기 서버 컴포넌트는 단계적으로 클라이언트에서 스트리밍됩니다. 먼저 폴백, 그 다음 최종 콘텐츠가 스트리밍됩니다.

이제 더 자세히 살펴봅시다!

클라이언트 사이드 렌더링

리액트의 가장 기본적인 렌더링 방법입니다. 요청 시 서버는 자바스크립트 번들을 참조하는 <script> 태그가 있는 기본적인 형태의 html 파일로 응답을 보냅니다. 자바스크립트가 로드되고 실행되면 페이지에 콘텐츠를 생성하고 빈 html 파일을 채웁니다. 네비게이션은 완전히 클라이언트 측에서 이루어지며 서버에 추가 요청을 하지 않으므로 Suspense의 첫 번째 사용 사례로 이어집니다. 자바스크립트 번들에는 앱의 모든 부분을 생성하는 데 필요한 코드가 포함되어 있기 때문에 상당히 커질 수 있습니다. 페이지의 콘텐츠가 렌더링되기 전에 전체 자바스크립트 파일을 로드, 구문 분석 및 실행해야 하므로 이는 심각한 성능 병목 현상이 됩니다. 물론 모든 페이지에서 앱의 모든 부분을 구성하는 코드가 필요한 것은 아닙니다. 대신 앱을 여러 개의 다른 자바스크립트 번들로 분할하여 각각 필요할 때만 클라이언트에 전송할 수 있다면 어떨까요? SuspenseReact.lazy를 사용하세요.

React.lazy와 Suspense

React.lazy의 핵심은 컴포넌트로 리졸브되는 Promise를 반환하는 함수를 전달하여 리액트 컴포넌트를 지연 로드할 수 있게 해줍니다. 하지만 대부분의 경우 동적 가져오기(dynamic import) 구문과 함께 다른 모듈을 지연 로드하는 데 사용됩니다.

const Post = lazy(() => import('./Post.ts'));

Suspense와 함께 사용하면 가져오기가 로드되는 동안 폴백 로딩 상태를 렌더링하도록 리액트에 지시할 수 있습니다.


export default function Wrapper() {
  return (
    <Suspense fallback={<div>Loading ...</div>}>
      <Post />
    </Suspense>
  )
}

리액트 라우터와 같은 네비게이션 라이브러리를 사용하는 경우, 앱 경로별로 코드 분할하여 Route 컴포넌트에서 각 페이지의 진입점을 개별적으로 지연 로드할 수 있습니다.

컴포넌트를 동적으로 불러오는 동안 로딩 상태를 렌더링하는 동작은 SuspenseReact.lazy 없이 직접 구현할 수도 있지만, Suspense를 사용하는 것이 훨씬 우아합니다. 하지만 Suspense를 사용해 useEffect에서 수행되는 모든 데이터 페칭을 단순화할 수 있다면 어떨까요?

useEffect에서 데이터 페칭을 위한 Suspense

더 알아보기 전에 <Suspense />의 내부를 간단히 살펴볼 차례입니다. 주로 부모인 <Suspense>가 자식이 로드 중인지 어떻게 알 수 있을까요? 제가 알기로는 자식 컴포넌트가 부모의 상태를 변경할 수 있는 방법은 두 가지뿐입니다.

  1. 자식이 부모에서 사용된 상태의 일부를 변경
  2. 자식이 부모가 감지하고 처리할 수 있는 값을 던짐

이 두 번째 옵션은 보통 에러 바운더리라는 형태로 제공되는데, 이는 앱이 중단될 때 자식이 던지는 의도하지 않은 에러를 포착하도록 설계된 리액트 컴포넌트입니다. 흥미롭게도 리액트는 이 메커니즘을 에러를 던지는 것 이상의 용도로 사용했습니다. Suspense는 자식이 Promise를 던지는 것에 의존합니다.

간단히 말해, 자식은 로딩하는 동안 펜딩 상태인 Promise를 던지고 렌더링할 준비가 되면 리졸브 합니다. 부모는 이 Promise를 포착하고 그에 따라 폴백 프로퍼티나 자식 콘텐츠를 렌더링합니다.

여러분이 상상하는것과 같이 서스펜스에서 지원하는 데이터 페칭 유틸을 직접 설정하는 것은 상당히 복잡할 수 있으며, 라이브러리 차원에서 처리하도록 하는 것이 좋습니다.

라이브러리가 이를 지원한다면 컴포넌트를 <Suspense />로 감싸고, 폴백을 지정하고, 에러 바운더리를 추가하여 거부된 Promise를 포착할 수 있습니다. 이렇게 하면 더이상 isLoading 또는 isError 상태에 대해 걱정할 필요가 없습니다!

function Post() {
  // React Query 예시
  const { data } = useQuery({ suspense: true })

  // 데이터가 로드될 때 리액트가 컴포넌트를 "일시중단"하고 이 코드가 실행되지 않기 때문에 데이터가 완전히 페치될 것이라고 가정할 수 있습니다.
  return <div>{data}</div>
}

export default function Wrapper() {
  return (
    <ErrorBoundary fallbackRender={<div>Error!</div>}>
      <Suspense fallback={<div>Loading ...</div>}>
        <Post />
      </Suspense>
    </ErrorBoundary>
  )
}

어떤 사람에게는 이것이 더 간단해 보일 수 있지만, 다른 사람은 여러 수준으로 중첩된 들여쓰기를 보고 로딩 및 오류 상태를 직접 처리하는 것을 선호할 수도 있습니다. 어느 쪽이든, Suspense를 사용한 데이터 페칭이 직접 구현할 수 없었던 새로운 가능성을 열어준 다는 점에는 이의를 제기하기 어렵습니다.

서버 사이드 렌더링

서버 사이드 렌더링 앱에서 Suspense는 매우 흥미로운 새로운 기능을 제공하기 시작하지만, 먼저 하이드레이션의 기본 사항에 대해서 설명드리겠습니다.

요청 시 메타프레임워크는 관련 파일에서 내보낸 컴포넌트를 실행하여 첫 번째 렌더링에 대한 html을 생성함으로써 지정된 페이지의 html을 생성합니다. 이 html은 사용자에게 전송되어 자바스크립트 번들이 로드되는 동안 의미 있는 내용을 볼 수 있습니다. 자바스크립트가 도착하면 메타프레임워크는 클라이언트에서 컴포넌트를 다시 실행하고 돔의 결과가 서버에서 생성된 html과 동일한지 확인합니다. 동일하지 않은 경우, 경고가 표시될 수 있습니다. 이 시점에서 서버에서 생성된 것과 동일한 돔을 갖게 되었지만 상태 생성, 이벤트 바인딩 등과 관련된 모든 자바스크립트는 동작하지 않습니다. 클라이언트에서 컴포넌트를 다시 실행하는 이 전체 프로세스를 하이드레이션이라 합니다.

클라이언트 사이드 렌더링에 비해 서버 사이드 렌더링은 자바스크립트 번들이 로드되고 실행되는 동안 사용자가 서버에서 생성된 일부 html을 볼 수 있으므로 첫 페이지 로드 시 더 나은 사용자 경험을 제공합니다. 그러나 자바스크립트가 없으면 페이지와 상호 작용할 수 없기 때문에 보기만 할 수 있습니다. 이 문제를 더욱 확대하면 페이지 전체에 하이드레이션을 해야만 페이지와 상호 작용할 수 있다는 사실입니다. 이로 인해 Suspense의 세번째 사용 사례인 선택적 하이드레이션이 나타납니다.

Suspense로 컴포넌트를 감싸면 리액트는 해당 컴포넌트를 페이지의 나머지 부분과 별도로 하이드레이션 합니다. 언뜻 보기에는 선택적 하이드레이션과 상관없이 모든 하이드레이션된 html이 클라이언트에 함께 전송되면 전체 페이지가 동시에 하이드레이션될 것이기 때문에 이 방법이 그다지 유익하지 않아 보일 수 있습니다. 하지만 이는 두 가지 이유로 정확하지 않습니다.

  1. 여러 컴포넌트를 Suspense로 감싸는 경우, 리액트는 사용자가 현재 어떤 컴포넌트와 상호작용하고 있는지에 따라 어떤 컴포넌트를 먼저 하이드레이트할지 현명하게 결정할 수 있습니다. 다시 말해, 리액트는 페이지의 어느 섹션에 우선순위를 정하고 사용자에게 상호 작용 가능한 위젯을 제공한 후 백그라운드에서 페이지의 나머지 부분에 하이드레이션할 수 있습니다. 자바스크립트 구문 분석 및 실행에 병목 현상이 발생할 수 있는 느린 기기에서는 사용자에게 더 빠른 경험을 제공할 수 있습니다.

  2. 스트리밍 아키텍처를 사용하면 페이지의 여러 부분을 클라이언트에 개별적으로 전송할 수 있으므로 페이지의 다른 부분이 서버에서 렌더링되는 동안 특정 html 청크를 클라이언트에 전송하여 선택적으로 하이드레이션할 수 있습니다! 스트리밍에 대해 자세히 알아보겠습니다.

현재 SSR 프레임워크는 선택적 하이드레이션을 지원하지 않으며, 앱 디렉토리를 사용하는 Next.js 애플리케이션에서만 서버에서 html로 렌더링되는 클라이언트 컴포넌트에 대한 선택적 하이드레이션을 지원합니다. 자세한 내용은 SSR과 서버 컴포넌트 비교 글을 확인하세요!

서버 컴포넌트

간단히 말해, 서버 컴포넌트는 클라이언트로 전송되기 전에 서버에서 html로 렌더링하는 리액트 컴포넌트입니다. 서버 사이드 렌더링처럼 들릴 수 있지만 서버 컴포넌트는 서버 전용이며 클라이언트에서 실행되지 않습니다. 이벤트 핸들러, 상태 또는 훅을 사용할 수 없으며 근본적으로 상호작용하지 않습니다. 대신 서버 컴포넌트는 정적 데이터를 가져오고 렌더링하는 데 최적화되어 있습니다.

export default async function Post() {
  const data = await fetch(...)

  return <div>{data}</div>
}

함수/컴포넌트가 비동기식이라는 점에 주목하세요! 데이터가 로드될 때까지 기다린 다음 콘텐츠를 html로 렌더링하여 클라이언트로 전송할 수 있습니다. 비동기 작업이 완료될 때까지 컴포넌트가 차단되고 사용자는 컴포넌트에서 아무것도 볼 수 없습니다. 사실 이러한 상황을 위해 로딩 상태가 만들어진 것입니다. 그렇다면 어떻게 서버 컴포넌트에 로딩 상태를 부여할 수 있을까요? 바로 Suspense를 사용하면 됩니다.

비동기 서버 컴포넌트를 <Suspense />로 감싸면, 리액트는 컴포넌트가 불러오는 동안 폴백을 렌더링하고 클라이언트로 보냅니다. 데이터 로딩이 완료되면 컴포넌트 자체에서 렌더링된 콘텐츠를 전송합니다. 시간이 지만에 따라 여러 개의 html 청크를 클라이언트에 전송하는 이 프로세스를 스트리밍이라고 합니다.

async function Post() {
  const data = await fetch(...)

  return <div>{data}</div>
}

export default function Wrapper() {
  return (
    <Suspense fallback={<div>Loading ...</div>}>
      <Post />
    </Suspense>
  )
}

결론

지금까지 세 가지 아키텍처에 대한 네 가지 리액트 Suspense 사용 사례를 살펴봤습니다. 단일 API가 다양한 상황에 적합한지는 여러분의 판단에 맡기겠지만, 이 글을 통해 상황을 명확하게 파악할 수 있기를 바랍니다. 읽어주셔서 감사합니다!

참고

profile
FE Engineer

2개의 댓글

comment-user-thumbnail
2023년 12월 21일

정말 도움이 되는 글이었습니다!
좋은 글 감사합니다.

답글 달기
comment-user-thumbnail
2024년 3월 28일

wink mod apk is meticulously designed with simplicity in mind, ensuring seamless video editing capabilities.

답글 달기