Frontend

Next.js 14, React 18의 streaming과 suspense을 이해해보자

코구리 2024. 8. 18. 18:00

Next.js을 공부하면서 또 반한 부분이 있는데 서버 측에서의 data fetching이다. 리액트에서는 클라이언트에서 API에 데이터를 요청하면 서버를 호출한다. 따라서 사용자가 페이지에 접속하면 로딩 상태인 것을 알 수 있다.

하지만 서버컴포넌트에서는 서버에 호출을 하기 때문에 API가 클라이언트에 노출되지 않는다. 따라서 DB와도 직접 통신할 수 있다.

 

그리고 React 18 버전부터 streaming과 suspense를 지원하고, Next.js가 이것을 활용해서 loading 상태를 간편하게 설정해줄 수 있게 되었다. 뿐만 아니라 한 페이지에 여러 API를 호출할 때도 먼저 완료된 작업부터 렌더링을 수행할 수 있게 했다.

 

1. streming이란?

https://developer.mozilla.org/ko/docs/Web/API/Streams_API

 

Streams API - Web API | MDN

Streams API는 Javascript를 이용해 네트워크를 통해 전송된 데이터 스트림에 접근하여 원하는 대로 처리가 가능한 API를 제공합니다.

developer.mozilla.org

 

Streaming은 네트워크를 통해 받은 리소스를 작은 조각으로 나누어, Bit 단위로 처리합니다.

 

공식문서에 따르면 이렇게 말하고 있다. fetch로 받아온 데이터를 작은 단위로 처리할 수 있는 것으로 이해했다.

HTML 자체가 청크 단위로 전송될 수 있는 특성을 가지고 있기 때문에, React는 이 특성을 활용하여 부분적으로 HTML을 전송하고, 이를 통해 초기 렌더링 속도를 개선한다고 한다.

 

그럼 리액트에서도 스트리밍을 사용해서 렌더링을 최적화할 수 있는거 아닌가? 꼭 넥스트를 사용해야 할 이유가 있을까? 알아보니 스트리밍은 서버가 클라이언트에 HTML을 부분적으로 전송하는 과정에서 발생한다. 이 과정은 서버에서 HTML을 생성하고, 이를 점진적으로 클라이언트로 보내는 방식이다. 이 때문에 스트리밍을 사용하려면 SSR을 기본으로 해야 합니다. 라고 한다.

클라이언트 사이드 렌더링이 기본인 리액트에서는 HTML을 모두 합쳐서 단 하나만 보낸다. 따라서 HTML을 부분적으로 전송한다는 스트리밍의 의미와 상충된다.

따라서 리액트에서 기본적으로 CSR로는 스트리밍 기능을 사용할 수가 없다.

 

2. 리액트에서의 로딩 컴포넌트

리액트에서 API 호출을 할 때는 클라이언트 측에서 서버에 요청을 한다. 따라서 받아오는 중에는 UI가 아래와 같이 빈화면으로 뜨게 된다. 

따라서 로딩 컴포넌트를 만들고 싶으면 로딩 상태를 관리하는 state를 만들어줘서 일일이 작성해줘야 한다.

로딩 중 UI
호출 완료 UI

 

리액트에서는 로딩 상태를 관리하기 위해 아래와 같이 (isLoading) state의 상태를 변경함으로써 조건부 렌더링을 수행할 수 있다.

export default function HomePage() {
  const [movies, setMovies] = useState([]);
  const [isloading, setIsLoading] = useState(true);

  async function getMovies() {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    const response = await fetch(API_URL);
    const json = await response.json();
    setMovies(json);
    setIsLoading(false);
  }

  useEffect(() => {
    getMovies();
  }, []);

  return (
    <div className={styles.container}>
      {isloading ? (
        <h1 className={styles.h1}>is Loading...</h1>
      ) : (
        movies.map((movie: Movie) => (
          <Movie key={movie.id} id={movie.id} title={movie.title} poster_path={movie.poster_path} />
        ))
      )}
    </div>
  );
}

로딩 상태

 

위 사진과 같이 로딩 중일 때 이렇게 관리할 수 있다.

나쁘지 않은 방법이지만 Next.js에서는 useState나 useEffect 없이 훨씬 쉽게 로딩 상태를 관리할 수 있다.

 

3. Next.js 에서의 로딩 컴포넌트

Next.js 에서는 이 스트리밍 기능을 활용한다.

API호출이 끝난 후에 HTML을 전달하기 때문에 작업이 끝나기 전에는 아무런 UI를 볼 수가 없다. 클라이언트에서 호출하면 빈 UI라도 보여줬지만 서버에서 호출하면 완료되기 전까지 아무런 화면을 띄우지 않는다.

따라서 로딩 중이라는 것을 알 수가 없기 때문에 이것을 Loading 컴포넌트로 만들어줄 수 있다.

 

그러면 사용자가 처음 페이지에 도착했을 때는 백엔드에서 완료되기 전까지 Loading 컴포넌트를 UI로 설정해준다. 이후 백엔드에서 작업이 완료되면 새로운 content로 교체해준다.

 

page.tsx이 속한 같은 폴더 내에 Loading.tsx 를 만들어준다. page도 그렇지만 loading도 반드시 이름을 지켜야 한다.

 

page.tsx

async function getMovies() {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  const response = await fetch(API_URL);
  const json = await response.json();
  return json;
}

export default async function HomePage() {
  const movies = await getMovies();
  return (
    <div className={styles.container}>
      {movies.map((movie: Movie) => (
        <Movie key={movie.id} id={movie.id} title={movie.title} poster_path={movie.poster_path} />
      ))}
    </div>
  );
}

 

loading.tsx

export default function Loading() {
  return <h2>Loading...</h2>;
}

 

자 어떤게 달라졌을까?

  • loading.tsx내에서 컴포넌트를 만들면 로딩 상태에 이 컴포넌트를 자동으로 띄워준다. 즉 상태를 직접 관리하기 위해 조건부 코드를 쓰거나 useState를 쓸 필요가 없다.
  • useEffect를 쓰지 않고 컴포넌트 자체에 async를 붙여서 페이지에 도착했을 때 API를 호출할 수 있다.

이렇게 해서 코드가 완전 단순해졌다. 앞서 말했듯이 streaming을 사용해서 가능한 일이다.

그리고 여러 API를 호출할 때 최적화하는 방법도 있는데 이건 다음번에 이어서 써보려고 한다.

 

아무튼 알면 알수록 놀라운 Next.js다. 이것만 있으면 최적화 할 필요 없을수도? 라는 생각도 드는데 아직 그건 아니겠지..