Next 25장 - 기초부터 다시 배우기 v14 (2)
포스트
취소

Next 25장 - 기초부터 다시 배우기 v14 (2)

Next

기존 fetching data

  • 기존의 데이터 페칭은 클라이언트에서 일어난다.
  • 가져온 데이터로 useState를 설정하고, 설정된 데이터를 활용하여야 하며 이러한 작업은 클라이언트 컴포넌트, 즉 브라우저가 API 요청을 보내는 것을 기반으로 하며, 브라우저에서 보내는 API 요청은 필연적으로 Loading 상태를 사용자가 인지할 수 있도록 추가적인 작업이 필요하다.
  • 또한 브라우저에서 데이터 요청을 보내기 때문에 네트워크 탭에서 API KEY에 대한 접근이 쉬워지고, 보안 측면에서도 안전할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"use client";

import { useState, useEffect } from "react";

export default function Example() {
  const [movies, setMovies] = useState([]);
  const [loading, setLoading] = useState(true);

  const getMovie = async () => {
    const res = await fetch("https://.../movieUrl");
    const json = await res.json();

    setMovies(json);
    setLoading(false);
  };

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

  return <div>{loading ? "Loading..." : JSON.stringfy(movies)}</div>;
}

server fetching

  • 서버 측에서 데이터를 가져오는 작업을 실행 후 사용자에게 보여주기만 한다면 이 모든 작업을 줄일 수 있다.
  • 서버에서 데이터를 페칭 후, 결과를 사용자에게 보여주기 떄문에 이 모든 작업이 진행되는 loading 상태와 데이터를 설정하기 전까지의 state, 상태값이 변함에 따라 설정하는 setState, 이 모든 작업동안 발생하는 rendering 등의 모든 작업을 단축시킬 수 있다.
  • 또한 이 작업은 캐싱되기 떄문에 Next.js가 캐싱된 데이터를 기억하고 또 다시 fetching 발생하지 않는다.
  • 위의 과정동안 브라우저에서 발생한 것은 없기 떄문에 네트워크 탭에서도 API 요청이 발생하지 않게 된다.
  • 컴포넌트가 async로 작성되어야 하는 이유는, Next.js가 페이지를 작은 HTML로 나누어 완료된 부분을 사용자에게 보여주게 되고, 데이터 페칭이 완료된 후 보여주어야 하는 컴포넌트가 아직 준비되지 않았다는 것을 알려주는 것이다.
1
2
3
4
5
6
7
8
9
10
11
const URL = "http://.../movieUrl";

async function getMovie() {
  return await fetch(URL).json();
}

export default async function Example() {
  const movies = getMovie();

  return <div>{JSON.stringfy(movies)}</div>;
}

loading

  • 서버 컴포넌트에서 실행하는 데이터 페칭이 극단적으로 느려진다면 어떻게 될까
  • 서버에서 실행한 데이터 페칭이 완료되기 전까지 사용자에게는 어떠한 UI도 주어지지 않는다.
  • 만약 데이터를 가져오기까지 5초가 걸린다고 한다면 5초동안 사용자는 빈 화면에서 탭의 로딩 상태를 나타내는 스피너만 보고 있어야 한다.
  • 이러한 UX를 방지하고자 Next.js에서는 특별한 기능을 사용할 수 있다.
  • loading 명을 가진 파일은 서버가 실행되는 동안의(loading 상태) 보여질 페이지로 인식한다.
  • 이렇게 작성된 loading 파일은 서버가 실행되는 동안 <div>데이터를 가져오고 있습니다.</div>를 보여주게 되고, fetching이 끝난 이후에는 Example.tsx 를 정상적으로 보여주게 되기에 사용자 경험을 해치지 않을 수 있다.
1
2
3
4
5
6
7
8
9
10
11
// page.tsx => 위의 Example.tsx
async function getMovie() {
  // 데이터 fetch가 발생하기 까지의 5초 설정
  await new Promise((resolve) => setTimeout(resolve, 5000));
  return await fetch(URL).json();
}

// page.tsx가 있는 디렉토리의 loading.tsx
export default function Loading() {
  return <div>데이터를 가져오고 있습니다.</div>;
}

최적화 시키기

  • getMovie와 같이 다른 데이터 페칭이 동시에 일어나는 경우에는 어떻게 할까
  • async await를 사용하고 있기 때문에 직렬적으로 작성하면 앞의 fetching이 필수적으로 끝나야만 뒤의 fetching이 실행된다.
  • 이를 병렬적으로 사용하기 위해 await Promise.all 을 사용하여 동시에 작업을 처리할 수 있다.
    • 하지만 이 중 하나의 요청만이라도 에러가 발생하게 되면 catch로 빠져버리기 떄문에 이 또한 좋은 방법이 아니다.
    • 또한 모든 비동기 요청이 완료되어야만 resolve를 반환한다.
  • Movie 컴포넌트에서 movie와 video fetching을 전부 실행하는 것이 아닌, 데이터를 보여주는 역할만 할 수 있도록 독립적으로 분리할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Promise all 을 통한 동시 처리
// page.tsx
export const API_URL = "http://.../movie";

const getMovie = async (id: string) => {
  return await fetch(API_URL).json();
};

const getVideo = async (id: string) => {
  return await fetch(`${API_URL}/${id}`).json();
};

export default async function Movie({
  params: { id },
}: {
  params: { id: string },
}) {
  const [movie, video] = await Promise.all([getMovie(id), getVideo(id)]);

  return (
    <div>
      <div>{movie.title}</div>
      <div>{video}</div>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// fetching 분리
// movie-info.tsx
async function getMovie(id: string){
  ...
}

export default async function MovieInfo({id}: {id: string}){
  const movie = getMovie(id);
  return <div>{JSON.stringfy(movie)}</div>
}

// movie-video.tsx
async function getVideo(id: string){
  ...
}

export default async function MovieVideo({id}: {id:string}){
  const video = getVideo(id);
  return <div>{JSON.stringfy(video)}</div>
}

// page.tsx
export default async function Movie({params: {id}}: {params:{id:string}}){
  return (
    <div>
      <Suspense fallback={<h6>Movie info loading...</h6>}><MovieInfo id={id}/></Suspense>
      <Suspense fallback={<h6>Movie video loading...</h6>}><MovieVideo id={id}/></Suspense>
    </div>
  )
}
  • 이처럼 data fetching이 발생하는 컴포넌트와 분리하여 독립적인 컴포넌트로 사용 시, data fetching을 기다리지 않아도 되며, 모든 작업이 완료되는 동안 사용자에게 보여줄 다른 컴포넌트를 작성하는 등의 역할이 가능하다.

Error

  • 만약 위와 같은 데이터 요청에 대한 에러가 발생하면 어떻게 될까
  • 전체 페이지의 동작이 멈추게 되며 사용자는 어떤 일이 발생하고 있는 지 알 수 없다.
  • 이를 방지하기 위해 특정 페이지에서 에러가 발생 시 최소한의 네비게이션 혹은 다른 페이지를 보여주기 위해서, 어떠한 화면을 보여줄 것인지를 error 파일로 작성할 수 있다.
  • 에러 페이지에 대한 파일은 각 페이지 폴더 내부에 작성해야 하며, 컴포넌트명은 바뀌더라도 파일명은 error를 유지해야 한다.
  • 이렇게 작성된 파일에 대해서 프레임워크가 인식하여 에러 페이지를 내보내게 된다.
  • 페이지에 대한 레이아웃은 유지된다.
  • 클라이언트 컴포넌트로 작성되어야 하기 때문에 use client를 작성해야 한다.
1
2
3
4
5
6
// /home/error.tsx
"use client";

export default function ErrExample() {
  return <div>서버 에러입니다.</div>;
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.