(참고) React App 에서의 데이터 페칭
Next 의 데이터 페칭을 알아보기 전에 React App 에서의 데이터 페칭은 아래 사진의 과정처럼 진행된다.
그러나 이 방식의 단점은 초기접속 요청으로 부터 데이터 로딩까지 오랜 시간이 걸린다는 점이다.
데이터 로딩이 긴 이유는 위의 사진의 3번에 해당하는 컴포넌트가 마운트 된 이후에 이루어진 이후에 데이터를 요청하기 때문이다.
Next.Js 의 데이터 페칭 : 사전 렌더링
- Next 는 React 와 달리 데이터 요청 및 로딩의 소요시간을 걱정하지 않아도 되는데,
- 이는 Next 에서 React의 느린 FCP 문제를 해결하기 위해서 사전 렌더링이라는 방식으로 동작하는데, 사전 렌더링 과정에서 백엔드 서버로부터 현재 페이지에 필요한 데이터를 미리 불러오도록 설정이 가능하기 때문이다.
- 그럼으로 서버가 브라우저에 전달하는 렌더링이 완료된 HTML 파일에는 이미 백엔드 서버로부터 불러온 데이터들이 다 포함되어 있어 사용자에게 데이터 패칭이 완료된 페이지를 추가적인 로딩 없이 한번에 보여줄 수 있다.
Next.Js 의 다양한 사전 렌더링
Next 의 사전 렌더링 과정이 오래걸릴 경우를 대비해서 Next 는 다양한 사전 렌더링 방식을 제공한다.
Next.Js : SSR(서버 사이드 렌더링)
서버 사이드 렌더링은 가장 기본적인 사전 렌더링 방식으로 요청이 들어올 때마다 사전 렌더링을 진행한다.
서버 사이드 렌더링을 위해서는 서버 사이드 렌더링을 적용할 페이지에
export const getServerSideProps = () => {};
위와 같은 화살표 함수를 만들어주면 이제 해당 페이지는 서버 사이드 렌더링 방식으로 동작하게 됩니다.
그럼 이제 페이지 url 로 접속하게 되면 getServerSideProps 함수가 동작해서 백엔드 서버로부터 데이터를 불러오고 페이지 컴포턴트가 실행된다.
즉, 컴포넌트 보다 먼저 실행되어 컴포넌트에 필요한 데이터를 불러오는 함수가 getServerSideProps 인 것이다.
export const getServerSideProps = () => {
const data = 'hello'
return {
props: {
data,
},
};
};
// InferGetServerSidePropsType : getServerSideProps 함수의 반환값 타입을
// 자동으로 추론해주는 기능을 하는 타입
export default function Home({data}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
(...)
)
}
그리고 백엔드 서버로부터 불러온 데이터는 props 로 컴포넌트로 전달한다.
이때, 한가지 주의할 점은 페이지 역할을 하는 컴포넌트는 브라우저로부터 접속요청을 받았을 때 사전 렌더링 과정(JS 렌더링)을 위해 서버 측에서 한번 실행되고, 브라우저에서 자바스크립트 번들 형래로 전달되는 수화(Hydration) 과정에서 한 번 더, 총 2번 실행된다.
그래서 window.loction 같은 브라우저에서만 동작하는 코드를 작성하면 오류가 발생한다.
이를 방지하고 브라우저에서만 동작하는 코드를 작성하고 싶을 때는 useEffect 를 이용하면 된다.
이는 useEffect 는 컴포넌트가 마운트된 이후에 동작하는 코드이기 때문이다.
API 호출하기
API 를 호출을 위한 코드를 getServerSideProps 함수안에 다 작성하면 너무 길기 때문에 따로 폴더를 구분하여 API를 요청하는 코드를 작성한다.
src > lib
모든 도서를 불러오는 api : fetchBooks.ts
import { BookData } from "@/types";
export default async function fetchBooks() : Promise<BookData[]>{
let url = `http://localhost:12345/book`
try {
const response = await fetch(url);
if(!response.ok){
throw new Error()
}
return await response.json()
} catch(err) {
console.error(err)
return [];
}
}
비동기 함수로 async, await 그리고 실패 및 에러 처리를 위해 try/catch 문 사용한다.
데이터 반환 타입으로 Promise<> 제네릭 문법을 사용한다.
함수 타입까지 정의가 완료되면 서버 사이드 렌더링을 사용하는 페이지로 이동하고 아래와 같이 코드를 수정한다.
export const getServerSideProps = async () => {
const allBooks = await fetchBooks();
return {
props: {
allBooks,
},
};
};
export default function Home({allBooks}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
(...)
)
}
또한 다른 API 요청를 추가해서 여러 요청이 있을 경우는
export const getServerSideProps = async () => {
const allBooks = await fetchBooks();
cosnt recoBooks = await fetchRandomBooks();
return {
props: {
allBooks,
recoBooks,
},
};
};
export default function Home({
allBooks,
recoBooks
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
(...)
)
}
이런식으로 작성해도 되지만, 하나씩 api 요청하는 것이 아니라 동시에 병렬적으로 api 를 요청하는 코드를 작성하고 싶으면 Promis.all 를 이용해서 아래와 같이 작성할 수도 있다.
export const getServerSideProps = async () => {
const [allBooks, recoBooks] = await Promise.all([
fetchBooks(),
fetchRandomBooks(),
])
return {
props: {
allBooks,
recoBooks,
},
};
};
검색을 담당하는 search 페이지에서는 getServerSideProps 함수를 아래와 같이 작성할 수 있다.
export const getServerSideProps = async (context : GetServerSidePropsContext) => {
const q = context.query.q
return {
props: {},
}
}
여기서 context 매개변수는 현재 브라우저로부터 받은 요청에 대한 모든 정보를 가지고 있어
url 에 /search?q="" 와 같이 쿼리 스트링으로 전달된 현재 검색어를 읽어올 수 있다.
이를 활용해서 새로운 api 요청 함수를 만들 수도 있지만, 모든 도서를 불러오는 fetchBooks 함수를 확장해서 사용하도록한다.
import { BookData } from "@/types";
export default async function fetchBooks(q?:string) : Promise<BookData[]>{
let url = `http://localhost:12345/book`
if (q){
url += `/search?q=${q}`
}
try {
const response = await fetch(url);
if(!response.ok){
throw new Error()
}
return await response.json()
} catch(err) {
console.error(err)
return [];
}
}
이런식으로 매개변수에 q를 전달하고 대신 모든 도서를 불러올때를 대비해서 매개변수 q를 선택적 프로퍼티로 설정하고
if문을 활용해서 q가 있을 경우에는 url를 검색 api 요청에 맞도록 수정해서 전달한다.
그리고 서치 페이지의 getServerSideProps 를 아래와 같이 수정합니다.
export const getServerSideProps = async (context : GetServerSidePropsContext) => {
const q = context.query.q
const books = await fetchBooks(q as string)
return {
props: {
books,
},
}
}
export default function Page({books}:InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div>
{books.map((book)=>(
<BookItem key={book.id} {...book} />
))}
</div>
)
}
또, context 를 활용하면 [id].tsx 같은 동적 라우팅 설정으로 설정된 페이지에서 api 요청을 통해 디테일 페이지를 구성할 수 있다.
export const getServerSideProps = async (context : GetServerSidePropsContext) => {
const id = context.params!.id;
return {
props: {}
}
}
url 파라미터의 정보는 context 매개변수에 params라는 프로퍼티에 담겨져 있어 const id = context.params!.id 로 작성하면되는데 이때 ! 를 해주는 이유는 context의 params는 Undefined일 수도 있기 때문에 ! 단어를 붙여준다.
import { BookData } from "@/types";
export default async function fetchOneBook(id:number) : Promise<BookData | null>{
const url = `http://localhost:12345/book/${id}`;
try {
const response = await fetch(url);
if(!response.ok){
throw new Error()
}
return await response.json()
} catch(err) {
console.error(err)
return null;
}
}
id 에 해당하는 api 호출은 위와 같이 작성하는데, 해당 함수는 여러개의 데이터가 아닌 하나의 데이터를 불러오는 함수이기 때문에, catch 문의 return 값은 [] 이 아니라 null 를 반환하도록 작성하고 fetchOneBook의 반환값 타입으로는 Promise<BookData | null>으로 수정해준다.
export const getServerSideProps = async (context : GetServerSidePropsContext) => {
const id = context.params!.id;
const book = await fetchOneBook(Number(id))
return {
props: {
book,
}
}
}
export default function Page({book}: InferGetStaticPropsType<typeof getServerSideProps>) {
if(!book) return "문제가 발생했습니다. 다시 시도해주세요.";
const {
id,
title,
subTitle,
description,
author,
publisher,
coverImgUrl
} = book
return (
(...)
)
}
그리고 실패할 경우 book은 null 일수도 있기 때문에, if(!book) return "문제가 발생했습니다. 다시 시도해주세요." 이런식으로 예외처리 해준다.
'Next.js' 카테고리의 다른 글
NextJs : ISR 사전 렌더링 (0) | 2024.09.20 |
---|---|
NextJs : SSG 사전 렌더링 (0) | 2024.09.19 |
NextJs : 글로벌/페이지 레이아웃 설정하기 (0) | 2024.09.13 |
NextJs : 네비게이팅/프리 페칭/API Routes/스타일링 (0) | 2024.09.12 |
NextJs : Page Router(페이지 라우터) (0) | 2024.09.12 |