
회사 프로젝트에 필요해져서 강의듣고 빠르게 정리!
정리해보자
1. Next.js 소개
Next.js는 React.js보다 더 강력하고 편하게 사용할 수 있는 기능들을 제공
- 페이지 라우팅
- 빌트인 최적화 기능
- 다이나믹 HTML 스트리밍
비교 기준 | 프레임워크 | 라이브러리 |
---|---|---|
기능 구현의 주도권 | 개발자에게 없다 | 개발자에게 있다 |
기능 제공 | 거의 모든 기능을 제공 | 기본기능 외 제공 X |
사전 렌더링 이해
- 브라우저의 요청에 사전에 렌더링이 완료된 HTML을 응답하는 렌더링 방식
- Client Side Rendering의 단점을 효율적으로 해결하는 기술
- 클라이언트(브라우저)에서 직접 화면을 렌더링 하는 방식 - 1. 접속 요청 - 2. index.html(빈 껍데기) - 3. 빈화면 렌더링 - 4. JS Bundle 리턴 - 5. JS실행 - 6. 컨텐츠 렌더링
- 장점 : 페이지 이동이 매우 빠르고 쾌적
- 단점 : 초기 접속 속도(FCP)가 느리다 - FCP(First Contentful Paint) : 요청 시작 시점으로부터 컨텐츠가 화면에 처음 나타나는데 걸리는 시간 - index.html, JS Bundle을 받아와야 하고, 실행까지 해줘야 함
- JS 실행 (렌더링) : 자바스크립트 코드(React 컴포넌트)를 HTML로 변환하는 과정
- 화면에 렌더링 : HTML코드를 브라우저가 화면에 그려내는 작업
- 수화(Hydration) : HTML과 Javascript를 연결하는 과정
- TTI (Time To Interactive)
- 클라이언트(브라우저)에서 직접 화면을 렌더링 하는 방식 - 1. 접속 요청 - 2. index.html(빈 껍데기) - 3. 빈화면 렌더링 - 4. JS Bundle 리턴 - 5. JS실행 - 6. 컨텐츠 렌더링
- 페이지 이동의 경우엔 클라이언트 렌더링 방식으로 처리
Next.js가 사전 렌더링으로 인해 얻는 장점
- React App의 단점 해소
- 빠른 FCP 달성
- React App의 장점 승계
- 빠른 페이지 이동
수강을 위한 백엔드 서버 설정
- github에서 소스 다운로드
- supabase 로그인
- .env 생성 후 설정
- npm install
- npx prisma db push
- npm run seed
- npm run build
- npm run start
npx prisma studio
-> 편하게 볼 수 있다.
2. Page Router 핵심 정리
2.1) Page Router를 소개합니다.
- 현재 많은 기업에서 사용되고 있는 안정적인 라우터
- React Router처럼 페이지 라우팅 기능을 제공
특징
- Pages의 폴더 경로와 파일 이름에 따라 라우팅
- 동적 경로는
[]
안에 넣어준다.
프로젝트 생성
npx create-next-app@14 section02
npm run dev
폴더 구조
- node_mudule : 의존성을 보관
- package.json : 패키지 내용 저장
- pages
- _app.tsx
- _document.tsx:
- 위 두개의 파일은 next앱의 모든 페이지에 공통적으로 적용 될 로직이나 레이아웃, 데이터를 다루기 위해 필요한 파일
- 메타 태그를 설정하거나, 서드파티 스크립트를 넣다던가 등등 페이지 전체에 적용
- pages
- next.config.mjs
- next app의 설정을 관리하는 파일
_app.tsx
- 모든 Page 컴포넌트의 부모 컴포넌트 역할 (root 컴포넌트)
- 인자값
- Component : 현재 페이지 역할을 할 컴포넌트를 받는다.
- pageProps : 페이지 역할을 할 컴포넌트에 전달될 props
결론적으로 next에서는 어떤 페이지를 렌더링 하던간에 App Component아래 Page역할을 하는 컴포넌트가 렌더링 된다.
_document.tsx
- 모든 페이지에 공통적으로 적용이 되야 하는 next app의 html코드를 설정하는 컴포넌트
2.2) 페이지 라우팅 설정하기
- QueryString
- 사용하기 위해선
useRouter
라는 훅을 next에서 가져와서 사용하면 된다.- 이름 그대로 Router라는 객체를 컴포넌트에서 사용할 수 있도록 반환하는 함수
back()
이나push()
처럼 페이지 이동 함수도 내장되어 있다.- query 프로퍼티 안에 객체 형태로 값이 저장되어 있다.
- 사용하기 위해선
import { useRouter } from 'next/router'
export default function Page() {
const router = useRouter()
// const q = router.query.q;
const { q } = router.query
return <h1>Search {q}</h1>
}
- url parameter
- queryString과 동일하게
useRouter()
을 사용하고 router객체에id
라는 프로퍼티로 저장된다. - 여러개의 url parameter를 주고 싶을 경우 ex - book/1234/234/243/234
[...id].tsx
로 만들어준다. (Catch All Segment라 부른다.)- 여러개의 id는 배열 형태로 저장된다.
- 파라미터가 없는 경우는 대응이 안되는데 해결책은 2가지
- index.tsx생성
[[...id]].tsx
대괄호를 2번 사용한다. (Optional Catch All Segment라 부른다.)
- queryString과 동일하게
import { useRouter } from 'next/router'
export default function Page() {
const router = useRouter()
const { id } = router.query
return <h1>Book</h1>
}
- 404Page
- page아래 만들면 된다.
2.3) 네비 게이팅
- 링크는 페이지를 새로 로드해 이동하는 방법 react에서는
<Link>
컴포넌트를 이용하는 방식이 좋다. - csr로 이동시키기 위해서는
useRouter
의 push를 이용한다.
import { useRouter } from "next/router";
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const onClickButton = () => {
router.push('/test');
};
...
}
2.4) 프리페칭
페이지를 사전에 미리 불러온다.
- 현재 사용자가 보고있는 페이지에서 이동 가능성이 있는 페이지를 모두 미리 불러온다.
- 컴포넌트로 명시된 경로가 아니라면 프리페칭이 이루어지지 않는다. - 사용하면 자동으로 프리페칭 이루어진다.
useRouter
의perfetch()
명시적으로 작성해주면 prefetch가 이루어 진다.- 컴포넌트를 prefetch하기 싫으면 인자값으로 `prefetch={false}`를 넘겨주면 된다.
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter();
const onClickButton = () => {
router.push('/test');
};
useEffect(() => {
router.prefetch('/test');
}, [])
...
}
2.5) API Routes
Next.js에서 API를 구축할 수 있게 해주는 기능
- /pages/api에 있는 경우 api응답을 정의하는 파일로서 자동으로 설정
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const date = new Date()
res.json({ time: date.toLocaleString() })
}
2.6) 스타일링
- React Component Styling하는 과정과 동일
- 인라인 방법
- css파일을 만들어 분리해주는 방법
- 글로벌 css는 App Component외에서는 css파일을 불러오는 방식을 지원하지 않는다.
- CSS Moudule 활용
- 활용하는 이유
- Next.js에서는 page별로 css가 겹치는 문제를 원천차단하기 위해서
- 활용하는 이유
import style from './index.module.css'
export default function Home() {
return (
<>
<h1 className={style.h1}>인덱스</h1>
<h2 className={style.h2}>H2</h2>
</>
)
}
import GlobalLayout from '@/components/global-layout'
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return (
<GlobalLayout>
<Component {...pageProps} />
</GlobalLayout>
)
}
여기서 @는 Next.js + TypeScript 프로젝트에서 자주 쓰는 **커스텀 경로 별칭(alias)**이다.
tsconfig.json이나 jsconfig.json을 보면
"paths": {
"@/*": ["./src/*"]
}
그래서 @/styles/globals.css = src/styles/globals.css
2.7) 글로벌 레이아웃 설정
- 글로벌 레이아웃은 루트 컴포넌트인 App컴포넌트에 적용시켜주면 된다.
- 페이지 컴포넌트를 감싸는 구조
특정 경우에만 컴포넌트를 감싸주고 싶을 때
import SearchableLayout from '@/components/searchable-layout'
import style from './index.module.css'
import { ReactNode } from 'react'
export default function Home() {
return (
<>
<h1 className={style.h1}>인덱스</h1>
<h2 className={style.h2}>H2</h2>
</>
)
}
Home.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>
}
- 메서드를 추가해 컴포넌트를 감싸주고 특정 경우에만 불러온다.
ReactNode
는 전달받는 컴포넌트의 타입- 페이지 역할
- _app.tsx : 앱 전체에 공통으로 적용되는 컴포넌트 (전역 설정)
- app Component로 어떤 요청이 오던 간에 루트 컴포넌트로써 렌더링 된다.
- 현재 접속요청이 온 페이지 컴포넌트들을
Component
라는 props로 전달을 받게 된다.
- 현재 접속요청이 온 페이지 컴포넌트들을
- app Component로 어떤 요청이 오던 간에 루트 컴포넌트로써 렌더링 된다.
- index.tsx : ”/“경로로 접근 시 렌더링 되는 페이지
- 페이지 연결은 Next.js내부 라우팅 시스템이 한다.
- _app.tsx : 앱 전체에 공통으로 적용되는 컴포넌트 (전역 설정)
import GlobalLayout from '@/components/global-layout'
import '@/styles/globals.css'
import { NextPage } from 'next'
import type { AppProps } from 'next/app'
import { ReactNode } from 'react'
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactNode) => ReactNode
}
export default function App({
Component,
pageProps,
}: AppProps & {
Component: NextPageWithLayout
}) {
const getLayout = Component.getLayout ?? ((page: ReactNode) => page)
return <GlobalLayout>{getLayout(<Component {...pageProps} />)}</GlobalLayout>
}
- 위와같이 전달받는데 null병합 연산자를 사용해 메서드가 정의되어 있지 않아도 사용 가능하게 한다.
- ((page: ReactNode) => page); 부분은 페이지를 받은 그대로 반환한다는 뜻
- AppProps는 Next.js의 _app.tsx 전용 타입 정의 app Component는 기본적으로 아래와 같은 구조이다.
type AppProps = {
Component: NextPage
pageProps: any
}
- compoent: NextPageWithLayout은 App Component가 받는 타입을 확장한 것
export default function SearchableLayout({
children,
}: {
children: ReactNode
}) {
const router = useRouter()
const [search, setSearch] = useState('')
// 기본적으로 query string이 string or string[]이다.
// 타입 단언으로 string으로 지정한다.
const q = router.query.q as string
useEffect(() => {
setSearch(q || '')
}, [q])
const onChangeSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value)
}
const onSubmit = () => {
if (!search || q === search) return
router.push(`/search?q=${search}`)
}
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onSubmit()
}
}
return (
<div>
<div>
<input
value={search}
onKeyDown={onKeyDown}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요..."
/>
<button onClick={onSubmit}>검색</button>
</div>
{children}
</div>
)
}
- React.ChangeEvent
는 TypeScript에서 DOM 이벤트에 타입을 명확하게 지정하는 방식
대표적인 이벤트 타입
요소 | 이벤트타입 |
---|---|
<input /> | React.ChangeEvent |
<textarea /> | React.ChangeEvent |
<select /> | React.ChangeEvent |
<form onSubmit={…}> | React.FormEvent |
<button onClick={…}> | React.MouseEvent |
<div onClick={…}> | React.MouseEvent |
2.8) 사전 렌더링과 데이터 페칭
- 기존 리액트 App에서의 패칭
-
- 불러온 데이터를 보관할 state생성
-
- 데이터 페칭 함수 생성
-
- 컴포넌트 마운트 시점에 fetchData호출
-
- 데이터 로딩중일때 예외처리
- => 이 방식의 단점 - 초기 접속 요청부터 데이터 로딩까지 오랜시간이 걸림
- 이유 : CSR방식의 FCP가 느리고, 게다가 마운트가 완료된 다음 호출하기 때문. (FCP + 데이터 로딩시간동안 기다려야 함)
-
- Next App의 데이터 페칭
- 사전 렌더링 중 발생함 (당연히 컴포넌트 마운트 이후에도 발생 가능)
- 데이터 요청 시점이 매우 빨라지는 장점 있음
- 추가적인 로딩없이 한방에 보여준다.
- 서버의 응답 시간이 너무 길 경우 빌드 타임에 데이터를 가져오는 설정도 가능
Next.js에서 제공하는 사전 렌더링 방식
-
- 서버사이드 렌더링(SSR)
- 가장 기본적인 사전 렌더링 방식
- 요청이 들어올 때 마다 사전 렌더링을 진행 함
-
- 정적 사이트 생성 (SSG)
- 빌드 타임에 미리 페이지를 사전 렌더링 해둠
-
- 증분 정적 재생성(ISR)
- 향후에 다룰 사전 렌더링 방식
2.9) 서버사이드 렌더링 (SSR)
export const getServerSideProps = () => {};
이 있으면 서버사이드 렌더링 방식으로 호출한다. (약속된 이름)- 페이지 컴포넌트보다 먼저 실행이 되어 필요한 데이터 불러오는 함수
- data를 props에 담아 리턴이 가능
- 리턴값은 props라는 단 하나의 객체여야만 한다.
- getServerSizeProps는 서버측에서만 실행하는 코드이다. (console 사용 불가)
- window객체에도 접근 못함 -> 브라우저로 접근을 못한다.
- 컴포넌트는 서버에서 2번 실행된다.
-
- 서버측 - 사전 렌더링 할 때
-
- 브라우저측 - 브라우저에서 자바스크립트 번들 형태로 전달되어서 브라우저에서 실행될 때 (하이드레이션)
- 이러한 이유 때문에
window
객체를 사용하지 못한다. ex - window.location- window.location같은 기능을 사용하고 싶을 땐
useRouter()
를 사용한다 - useEffect() 를 사용해 마운트 이후에 사용도 가능하다.
- window.location같은 기능을 사용하고 싶을 땐
-
- Component에서 props의 type을 지정할 땐 아래와 같이 InferGetServerSidePropsType을 사용
- Promise.all()을 사용할 경우 여러 프로미스를 병렬로 실행하므로 여러 개의 비동기 작업을 동시에 처리할 수 있다.
export const getServerSideProps = async () => {
// 컴포넌트보다 먼저 실행되어서, 컴포넌트에 필요한 데이터를 불러오는 함수
// const allBooks = await fetchBooks();
// const recoBooks = await fetchRandomBooks();
const [allBooks, recoBooks] = await Promise.all([
fetchBooks(),
fetchRandomBooks(),
])
return {
props: {
allBooks,
recoBooks,
},
}
}
export default function Home({
allBooks,
recoBooks,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div className={style.container}>
<section>
<h3>지금 추천하는 도서</h3>
{recoBooks.map(book => (
<BookItem key={book.id} {...book} />
))}
</section>
<section>
<h3>등록된 모든 도서</h3>
{allBooks.map(book => (
<BookItem key={book.id} {...book} />
))}
</section>
</div>
)
}
Home.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>
}
- query string 읽어오는 방법
- context : GetServerSideProps를 매개변수로 받는다
- GetServerSideProps에는 브라우저에서 받은 요청에 대한 모든 정보가 포함되어 있다.
- context : GetServerSideProps를 매개변수로 받는다
2.10) SSG
시작하기 전에
- SSR
- 장점 : 페이지 내부의 데이터를 항상 최신으로 유지할 수 있다.
- 단점 : 데이터 요청이 늦어질 경우 모든게 늦어진다.
- SSG
- SSR의 단점을 해결하는 사전 렌더링 방식
- 빌드 타임에 페이지를 미리 사전 렌더링 해 둠
- 단점 : 매번 똑같은 페이지만 응답, 최신 데이터 반영은 어렵다.
- SSG
export const getStaticProps = async () => {}
함수가 있으면 SSG방식으로 호출한다.- 받는 컴포넌트에서는
export default function Page({book,}: InferGetStaticPropsType<typeof getStaticProps>) {}
로 받아 사용한다InferGetStaticPropsType
와 똑같이 getStaticProps로 생성한 함수의 반환값 타입을 추론에서 PropsType으로 설정해주는 역할
- 수정했는데도 화면에 바로 반영되는 것을 볼 수 있는데 dev모드로 실행했기 때문
- npm run build
- static, SSG, Dynamic(SSR)을 볼 수 있는데 아무런 설정 안하고 route하면 SSR이 기본값이다.
- SSG방식은 SSR방식처럼 Context에서 query string을 가져올 수 없다.
- 제약사항이 있음에도 불구하고 SSG로 동작시키길 원한다면 사전 렌더링 과정 이후에 Client에서 직접 실행해줘야함
- 이전에 React의 방식으로
- 제약사항이 있음에도 불구하고 SSG로 동작시키길 원한다면 사전 렌더링 과정 이후에 Client에서 직접 실행해줘야함
- [id].tsx SSG
- 동적인 경로를 가진 파일을 SSG하기 위해선 선수 과정으로 경로를 설정하는 것이 필요하다.
- 해당 역할을 하는 것은
getStaticPaths()
- path라는 값으로 현재 어떤 경로들이 존재할 수 있는지 배열로 리턴해야 한다.
- url parameter값은 반드시 문자열로 명시해야 한다.
- fallback 옵션을 추가해야 한다 -> path값에 존재하지 않는 url로 접속 요청을 보내게 되면 설정할 대비책
- false : 404 Not Found반환
- blocking : 즉시 생성 (Like SSR)
- true : 즉시 생성 + 페이지만 미리 반환
- true로 해줬을 때 loading중이면
useRouter()
의isFallback
을 이용해 분기처리가 가능
- true로 해줬을 때 loading중이면
- data가 없을 경우 return {notFound: true}를 통해 에러처리가 가능하다
- 해당 역할을 하는 것은
- 동적인 경로를 가진 파일을 SSG하기 위해선 선수 과정으로 경로를 설정하는 것이 필요하다.
export const getStaticPaths = () => {
return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } },
{ params: { id: '3' } },
],
fallback: true,
// fallback : "blocking"
// fallback : false
}
}
export const getStaticProps = async (context: GetStaticPropsContext) => {
// 해당 페이지는 무조건 param이 있는 페이지
// 그로므로 !로 단언해줘도 상관 없다.
const id = context.params!.id
const book = await fetchOneBook(Number(id))
if (!book) {
return {
notFound: true,
}
}
return {
props: {
book,
},
}
}
export default function Page({
book,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter()
if (router.isFallback) return '로딩 중입니다.'
if (!book) return '문제가 발생 하였습니다.'
const { id, title, subTitle, description, author, publisher, coverImgUrl } =
book
return (
<div className={style.container}>
<div
className={style.cover_img_container}
style={{ backgroundImage: `url('${coverImgUrl}')` }}
>
<img src={coverImgUrl} />
</div>
<div className={style.title}>{title}</div>
<div className={style.subTitle}>{subTitle}</div>
<div className={style.author}>
{author} | {publisher}
</div>
<div className={style.description}>{description}</div>
</div>
)
}
2.11) ISR(Incremental Static Regeneration) 증분 정적 재생성
- SSG방식으로 생성된 정적 페이지를 일정 시간을 주기로 다시 생성하는 기술
- 매우 빠른 속도 응답 가능 (SSG 장점)
- 최신 데이터 반영 가능 (기존 SSR방식의 장점)
- revalidate 옵션을 준다. (아래 코드는 3초마다 재생성한다.)
export const getStaticProps = async () => {
// 컴포넌트보다 먼저 실행되어서, 컴포넌트에 필요한 데이터를 불러오는 함수
// const allBooks = await fetchBooks();
// const recoBooks = await fetchRandomBooks();
console.log('인덱스 페이지')
const [allBooks, recoBooks] = await Promise.all([
fetchBooks(),
fetchRandomBooks(),
])
return {
props: {
allBooks,
recoBooks,
},
revalidate: 3,
}
}
주문형 재검증
- 요청받을 때마다 페이지를 다시 생성
- 시간으로 생성하는 것 보다 자원 낭비가 적다.
- api를 생성하여 revalidate할 경로를 작성한다.
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
await res.revalidate('/')
return res.json({ revalidate: true })
} catch (err) {
res.status(500).send('Revalidation Failed')
}
await res.revalidate('/')
}
2.12) SEO 설정
- 메타 태그 설정을 위해 next/head의 import
export default function Home({
allBooks,
recoBooks,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
<Head>
<title>한입 북스</title>
<meta property="og:image" content="/thumbnail.png" />
<meta property="og:title" content="한입 북스" />
<meta property="og:description" content="한입 북스에 등록된 도서들을 만나보세요" />
</Head>
<title>한입 북스</title>
</Head>
<div className={style.container}>
<section>
<h3>지금 추천하는 도서</h3>
{recoBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</section>
<section>
<h3>등록된 모든 도서</h3>
{allBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</section>
</div>
</>
);
}
2.13) 배포하기
vercel에 배포
바로 배포하는 명령어
- vercel –prod
2.14) 페이지 라우터 정리
- Page Router의 장점
-
- 파일 시스템 기반의 간편한 페이지 라우팅 제공
- page폴더의 구조로 설정
- 동적 경로의 경우 []로 url파라미터에 대응
- book/100/2345/12233같은 경우 […id].tsx로 대응
- index page까지 대응하려면 [[…id]].tsx
- book/100/2345/12233같은 경우 […id].tsx로 대응
-
- 다양한 방식의 사전 렌더링 제공
- 느린 fcp에 대응하기 위해 서버에서 js를 실행해 fcp를 단축 (사전 렌더링)
- SSR, SSG, ISR로 나뉜다.
-
- Page Router의 단점
-
- 페이지별 레이아웃 설정이 번거롭다.
-
- 데이터 페칭이 페이지 컴포넌트에 집중된다.
-
- 불 필요한 컴포넌트들도 JS Bundle에 포함된다.
- 상호 작용이 없는 컴포넌트들도 js bundle에 포함되어 2번씩 실행된다.
-
3. App Router 시작하기
3.1) App Router 시작하기
- 변경되거나 추가되는 사항
- 페이지 라우팅 설정 방식 변경
- 레이아웃 설정 방식 변경
- 데이터 페칭 방식 변경
- React 18 신규 기능 추가
- ex - Streaming, React Server Component
- 크게 변경되지 않은 사항
- 네비게이팅 (Navigating)
- 프리페칭 (Pre-fetching)
- 사전 렌더링 (Pre-Rendering)
npx create-next-app@latest section03으로 프로젝트 생성
추가로 eslint.config.mjs 설정 변경
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
// 이 부분이 추가
rules: {
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
},
},
]
export default eslintConfig
3.2) 페이지 라우팅 설정하기
- App이라는 폴더를 기반으로 라우팅 설정
page.tsx
페이지 이름을 갖는 파일이 페이지 역할을 하는 파일로 자동 설정된다.- ~/search경로 만들고 싶으면 search폴더 아래에
page.tsx
파일을 생성해야 한다. - 동적 경로를 만들고 싶을 때는 [id] 같은 대괄호로 감싸진 폴더를 만든 다음에
page.tsx
파일을 생성한다
- App Router의 Next버전에서는 query string이나 url파라미터와 같은 경로상의 포함되는 값들은 Page컴포넌트에 props로 전달이 된다.
- 함수형 컴포넌트에 async를 붙이는데 리액트의 서버 컴포넌트이기 때문에 async를 붙일 수 있다.
- 아래는 query string과 param을 가져오는 코드이다.
- url파라미터가 중첩으로 여러개 전달되는 경우에도 대응하게 만들고 싶으면 […id]라고 폴더명을 변경한다.
- url파라미터가 없는 경우에도 대응하고 싶으면 [[…id]]로 만들어 준다.
export default function Page(props) {
console.log(props)
return <div>Search 페이지</div>
}
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q: string }>
}) {
const { q } = await searchParams
return <div>Search 페이지 : {q}</div>
}
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return <div>book/[id] 페이지 : {id}</div>
}
3.3) 레이아웃 설정하기
- 폴더 아래에
layout.tsx
가 있으면 search페이지의 레이아웃으로 자동 적용 - 폴더의 모든 경로로 시작하는 페이지의 레이아웃으로 적용된다.
- ex - /search/layout.tsx -> /search 모든 경로에 적용
- root layout이 의 로 나타나고 있다.
- html의 기초 틀을 유지할 수 있게 한다.
- children이라는 props로 자동으로 페이지 컴포넌트가 전달된다.
- layout component를 직접 구성할 때는 리턴문 안에 어디에 렌더링 할 것인지 지정해줘야 한다.
- 공통된 경로로 시작하지 않은 페이지에도 동일한 레이아웃을 설정하는 방법
- 라우트 그룹 활용 -> 폴더명을 소괄호로 감싸 만든다.
- ex - (with-searchbar)
- 더이상 경로에 어떠한 영향도 주지 않으면서, 각기 다른 경로를 가진 페이지 파일을 하나의 파일에 묶을 수 있는 기능
- 경로상에 영향을 미치지 않으면서 layout만 동일하게 적용이 가능하다.
- 라우트 그룹 활용 -> 폴더명을 소괄호로 감싸 만든다.
3.4) 리액트 서버 컴포넌트 이해하기
- React18v부터 추가된, 새로운 유형의 컴포넌트
- 서버측에서만 실행되는 컴포넌트 (브라우저에서 실행X)
- Page Router버전에서의 Next.js에는 어떤 문제가 있었나
- Hydration을 위해 모든 컴포넌트들을 JS Bundle로 묶어 보내준다.
- 리액트 훅이나 이벤트 핸들러가 붙어있는건 ok / 상호작용이 없는 정적인 컴포넌트도 포함하는건 불필요
- 문제점을 해결하기 위해선 사전 렌더링 과정중에 JS Bundle을 구성할 때 상호작용이 없는 정적인 컴포넌트를 제외한다
- 그렇게 되면 클라이언트 JS Bundle에 컴포넌트들만 포함된다.
- Hydration을 위해 모든 컴포넌트들을 JS Bundle로 묶어 보내준다.
- 페이지의 대부분을 서버컴포넌트로 구성할 것은 권장, 클라이언트 컴포넌트는 꼭 필요한 경우에만 사용
- 서버 컴포넌트는 서버측에서만 할 수 있었던 보안에 민감한 작업이나 데이터를 패칭해오는 다양한 작업을 진행할 수 있다.
- 기본적으로 server component, client component로 바꾸고 싶으면 페이지 최 상단에 “use client”를 기입한다.
'use client'
import Image from 'next/image'
import styles from './page.module.css'
import { useEffect } from 'react'
export default function Home() {
console.log('Home 컴포넌트 실행')
useEffect(() => {})
return <div className={styles.page}>인덱스 페이지</div>
}
- 상호작용이 있는 컴포넌트만 클라이언트 컴포넌트로 만들어준다. (Link는 HTML고유 기능)
- 페이지나 레이아웃이 아닌 컴포넌트를 위한 파일도 만들어 줬는데 파일 이름이 Page나 Layout이 아니면 일반적인 javascript or typescript파일로 간주
- 컴포넌트를 위한 파일도 app하위에 위치시켜도 된다. -> Co-Location이라 부른다.
3.5) 리액트 서버 컴포넌트 주의사항
-
- 서버 컴포넌트에는 브라우저에서 실행될 코드가 포함되면 안된다.
- React Hooks나 이벤트 핸들러는 사용 불가
- 브라우저에서 실행되는 기능을 담고있는 라이브러리도 사용 불가
-
- 클라이언트 컴포넌트는 클라이언트에서만 실행되지 않는다.
- 서버와 클라이언트 모두에서 실행된다.
-
- 클라이언트 컴포넌트에서 서버 컴포넌트를 import할 수 없다
- 클라이언트 컴포넌트는 서버와 브라우저 모두에서 실행된다.
- 만약 import해서 서버 컴포넌트를 사용한다면 해당 내용은 오직 서버에서만 실행된다. -> 코드가 없다
- 이런 경우 Next.js는 자동으로 서버 컴포넌트를 클라이언트 컴포넌트로 변경한다.
- 부득이하게 Client Component에서 Server 컴포넌트로 사용해야 하는 경우
- 바로 import해서 사용하는 것이 아니라 children props로 받아 렌더링을 시킨다.
- children으로 받은 컴포넌트는 클라이언트 코드로 변경하지 않는다.
- 결과물만 받게 구조가 변경되었기 때문
-
- 서버 컴포넌트에서 클라이언트 컴포넌트에게 직렬화 되지 않는 Props는 전달 불가하다.
- 직렬화 되지 않은 Props?
- 직렬화 : 객체, 배열, 클래스 등의 복잡한 구조의 데이터를 네트워크 상으로 전송하기 위해 아주 단순한 형태(문자열, Byte)로 변환하는 것을 의미한다.
- 자바스크립트의 함수는 직렬화가 불가능 -> 함수 전달 불가능
- 서버 컴포넌트가 Next.js에서 어떻게 실행되는지 살펴보면 이해가 쉽다
- 서버 컴포넌트는 사전 렌더링 때 실행된다.
- 페이지를 구성하는 모든 컴포넌트들 -> 서버 컴포넌트들만 따로 실행 -> Client Component실행-> 완성된 HTML페이지
- 서버 컴포넌트들만 실행이 될 때 RSC Payload(JSON과 유사한 형태)라는 문자열이 생성된다.
- RSC Payload
- React Server Component의 순수한 데이터 (결과물)
- React Server Component를 직렬화한 결과
- RSC Payload에는 서버 컴포넌트의 모든 데이터 포함
- 서버 컴포넌트의 렌더링 결과
- 연결된 클라이언트 컴포넌트 위치
- 클라이언트 컴포넌트에게 전달하는 Props값
- RSC Payload
- 함수는 직렬화가 안되기 때문에 RSC Payload에 포함되지 않는다. -> 함수는 props로 전달 불가능
3.6) 네비게이팅
- 페이지 이동은 Page Router와 동일한데 Client Side Rendering 방식으로 처리된다.
- pre-fetching에서 동작이 조금 달라진다.
- JS Bundle은 브라우저에서 실행할 자바스크립트 코드 (Client Components만 포함됨)
- 프로그래미틱한 페이지 이동 구현
useRouter()
를 사용한다- Page Router와 달리 useRouter를 App Router 버전인
next/navigation
에서 가져온다.
- Page Router와 달리 useRouter를 App Router 버전인
- App Router의 페이지는 static과 Dynamic페이지로 나뉜다.
- 기본적으로 static, query string, url parameter들을 꺼내던가 하는 작업이 있으면 Dynamic페이지로 설정된다.
- static은 RSC payload와 js bundle을 불러오고, dynamic 페이지는 data upload가 필요할 수도 있으니 RSC payload만 불러온다.
- dynamic 페이지는 JS bundle은 향후에 실제 페이지 이동이 발생했을 때만 불러온다
=> App Router버전에서는 페이지의 데이터가 Server Component는 RSC Payload로 Client Component는 JS Bundle로 나뉘어서 동시에 전달된다.
3.7) 한입북스 UI 구현하기
- useSearchParams() 라는 훅
- 현재 페이지에 전달된 query string값을 꺼내올 수 있다.
- PageRouter에서는 useRouter()를 사용해 router.query.q로 불러왔다.
- AppRouter는 불가능
- **useSearchParams()**를 사용해야 한다.
- 현재 페이지에 전달된 query string값을 꺼내올 수 있다.
4. 데이터 페칭
4.1) 앱 라우터의 데이터 페칭
- Page Router
-
- SSR (서버사이드 렌더링)
- export async function getServerSideProps() {}
-
- SSG (정적 사이트 생성)
- export async function getStaticProps() {}
-
- Dynamic SSG(동적 경로에 대한 정적 사이트 생성)
- export async function getStaticPaths() {}
- Page Router에서는 클라이언트 컴포넌트만 있기 때문에 서버와 클라이언트 모두 실행이 된다
- 위와같은 특수한 함수를 사용해 서버에서만 사용하게 코드를 작성했어야 했다.
- 클라이언트 컴포넌트에는 Async키워드를 사용할 수 없었다.
- 브라우저에서 동작 시 문제를 일으킬 수 있기 때문이다.
-
- App Router
- 컴포넌트 내부에서 직접 필요로 하는 데이터를 불러와서 바로 렌더링해서 사용할 수 있는 방식으로 변경
- 필요로 하는 곳에서 불러오니 props를 전달해서 사용한다는 Page Router의 단점이 아래와 같이 사라진다.
async function AllBooks() {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book`);
const allBooks: BookData[] = await response.json();
if(!response.ok) {
return <div>오류가 발생했습니다...</div>
}
return (
<div>
{allBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
// query string 사용
import BookItem from "@/components/book-item";
import { BookData } from "@/types";
export default async function Page({
searchParams,
}: {
searchParams: Promise<{
q?: string;
}>;
}) {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/search?q=${searchParams.q}`
);
if (!response.ok) {
return <div>오류가 발생했습니다...</div>;
}
const books: BookData[] = await response.json();
return (
<div>
{books.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
// params
export default async function Page({
params,
}: {
params: Promise<{ id: string | string[] }>;
}) {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/${params.id}`);
if (!response.ok) {
return <div>오류가 발생했습니다...</div>;
}
const book = await response.json();
const { id, title, subTitle, description, author, publisher, coverImgUrl } =
book;
...
}
- url 환경변수로 등록
NEXT_PUBLIC_API_SERVER_URL=http://localhost:12345
NEXT_PUBLIC
이 없으면 서버측 컴포넌트에서만 사용이 가능하다- 클라이언트 컴포넌트에서도 사용이 가능하려면
NEXT_PUBLIC
을 접두사로 붙여야 한다.
4.2) 데이터 캐시
- 데이터 캐시 : fetch메서드를 활용해 불러온 데이터를 Next 서버에서 보관하는 기능
- 영구적으로 데이터를 보관하거나, 특정 시간을 주기로 갱신시키는 것도 가능
- 불필요한 데이터 요청 수를 줄여서 웹 서비스의 성능을 크게 개선할 수 있다.
- ex - const response = await fetch(
~/apu
, { cache: “force-cache})- 요청 결과를 무조건 캐싱함
- 한번 호출 된 이후에는 다시는 호출되지 않음
- axios같은 http request라이브러리 에서는 사용 불가능하고 오직 fetch메서드에서만 활용 가능
- Next.js에서 제공하는 fetch는 일반적인 fetch메서드가 아닌 데이터 캐시와 관련된 여러 기능을 추가한 확장판개념의 메서드이기 때문
- 캐시 옵션
-
- {cache: “no-store”}
- 데이터 페칭의 결과를 저장하지 않는 옵션
- 캐싱을 아예 하지 않도록 설정하는 옵션
- 데이터 캐싱의 기본값은 캐싱을 하지 않는 것 (15버전 이하는 기본값이 캐싱하는 것)
-
- {cache: “force-cache”}
- 요청의 결과를 무조건 캐싱함
- 한번 호출 된 이후에는 다시는 호출되지 않음
-
- {next: {revalidate:3}};
- 특정 시간을 주기로 캐시를 업데이트
- 마치 Page Router의 ISR방식과 유사
-
- {next: {tags: [‘a’]}}
- On-Demand Revalidate
- 요청이 들어왔을 때 데이터를 최신화 함
-
fetch데이터 확인을 위한 config옵션
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
/* config options here */
logging: {
fetches: {
fullUrl: true,
},
},
}
export default nextConfig
4.3) 리퀘스트 메모이제이션
- 다양한 API요청 중에 중복된 요청을 제거시켜 캐싱시켜 한번만 요청할 수 있도록 자동으로 데이터 페칭을 최적화 해주는 기능
- 하나의 페이지를 렌더링 하는 동안에 중복된 API요청을 캐싱하기 위해 존재함
- 렌더링이 종료되면 모든 캐시가 소멸된다.
5. 페이지 캐싱
5.1) 풀 라우트 캐시1
Next서버 측에서 빌드 타임에 특정 페이지의 렌더링 결과를 캐싱하는 기능
SSG와 비슷하게 빌드타임에 페이지를 만들어 줘 바로 페이지 반환
- Dynamic Page로 설정되는 기준 (서버 컴포넌트만 해당, 클라이언트 컴포넌트는 페이지 유형에 영향을 미치지 않음)
- 특정 페이지가 접속 요청을 받을 때 마다 매번 변화가 생기거나, 데이터가 달라질 경우
-
- 캐시되지 않는 Data Fetching을 사용하는 경우
-
- 동적 함수(쿠키, 헤더, 쿼리스트링)을 사용하는 컴포넌트가 있을 때
- static page로 설정되는 기준
- Dynamic페이지가 아니면 모두 static page가 된다.
동적함수 사용 유무 | 데이터캐시 | 페이지 분류 |
---|---|---|
YES | NO | Dynamic Page |
YES | YES | Dynamic Page |
NO | NO | Dynamic Page |
NO | YES | Static Page |
- 맨 마지막의 동적사용 x, 데이터 캐시o -> 풀 라우트 캐시가 가능하다
- 풀 라우트 캐시도 revalidate가 가능하다.
5.2) 풀 라우트 캐시2
- useSearchParams() -> Suspense로 감싸줘야 한다.
- Suspense태그로 감싸주게 된다면 사전 렌더링 과정에선 배제되고, 오직 클라이언트 측에서만 렌더링 된다.
5.3) 풀 라우트 캐시 - 동적 경로에 적용하기
- 실시간으로 데이터를 백엔드 서버로 부터 불러와서 렌더링하는 페이지는 static으로 설정 불가능
- 풀 라우트 캐시를 포기하지만, 조금이라도 생성 속도를 최적화를 하자면 데이터 캐시만 따로 적용하는 방법이 있다.
- parameter로 받는 경우, 어떤 경로가 올 수 있는지 알려줘야 한다. -> 빌드타임에 만들어 질 수 있도록
generateStaticParams()
를 활용한다- 정적인 url파라미터를 담은 배열을 반환하면 빌드타임에 넥스트 서버가 자동으로 정적 파라미터들을 읽어서 파라미터에 해당하는 페이지를 정적으로 만들게 된다.
- 명시할 때는 문자열로만 명시해야 한다.
- 데이터 캐싱이 설정되지 않은 데이터 패칭이 존재하더라도 무조건 해당하는 페이지가 static페이지로 강제로 설정된다.
- 404오류일 경우엔 nextjs의 notFound() 를 활용해 404페이지로 리다이렉트 해준다.
- app폴더 아래에 not-found.tsx를 만들어준다.
- 설정하지 않은 페이지들도 접속하면 static한 page를 만드는데 그렇지 않고 정적으로 설정해둔 url파라미터 이외에 전부 에러처리를 하려면
- export const dynamicParams = false; 를 줘서 내보내주면 된다. (기본값이 true)
- dynamicParams이 true일 경우 요청 오면 새로운 페이지를 실시간으로 생성해서 응답
export const dynamicParams = false;
export function generateStaticParams() {
return [{id: "1"}, {id: "2"},{id: "3"}];
}
export default async function Page(props: {
params: Promise<{ id: string | string[] }>;
}) {
const { id } = await props.params;
...
}
5.4) 라우트 세그먼트 옵션
- 풀라우트 캐시를 위해서 모든 페이지의 컴포넌트들이 동적인 함수를 사용하진 않는지, 또는 캐싱되지 않는 데이터 페칭을 하고있지 않는지 확인해야 한다.
- 라우트 세그먼트 옵션 : 강제로 특정 페이지를 스태틱, 다이나믹 페이지로 설정하던지, Page Revalidate를 하던지 페이지를 동작을 강제로 설정할 수 있다.
dynamicParams
처럼 약속된 이름의 변수를 선언하고, 값을 설정함으로써 페이지의 설정을 강제로 조절할 수 있는 기능 -> 라우트 세그먼트 옵션- 많이 사용되는 옵션은
Dynamic옵션
- export const dynamic = ”;
- dynamic옵션을 사용하면 페이지 내부의 동적함수나 데이터 캐시의 유무를 떠나서 강제로 static or dynamic으로 설정 가능
-
- auto : 기본값으로 아무것도 강제하지 않음 (기본값이니 생략 가능)
-
- force-dynamic : 페이지를 강제로 Dynamic 페이지로 설정
-
- force-static : 페이지를 강제로 static 페이지로 설정
- 페이지 내부의 동적함수는 빈값을 리턴하게 자동으로 설정함
-
- error : 페이지를 강제로 Static 페이지로 설정 (설정하면 안되는 이유가 있으면 -> 빌드 에러)
-
- 라우트 세그먼트 옵션을 지정해주는건 권장사항이 아니다.
- App Router기반의 NextJS은 페이지를 구성하고 있는 각각의 컴포넌트들이 어떻게 동작하는지에 따라서 컴포넌트 단위로 페이지를 스태틱, 다이나믹으로 설정해주는 아주 좋은 기능이 있는데 이러한 매커니즘을 무시하기 때문
5.5) 클라이언트 라우터 캐시
- 브라우저에 저장되는 캐시
- 페이지 이동을 효율적으로 진행하기 위해 페이지의 일부 데이터를 보관함
- 레이아웃에 대한 데이터를 따로 추출을 해서 클라이언트 라우터 캐시라는 이름으로 보관하도록 자동으로 설정한다.
- 한번 접속한 페이지의 레이아웃만 따로 보관해 페이지 이동이 추가로 발생하였을 때 공통된 레이아웃을 중복해 불러오지 않도록 페이지의 이동을 최적화
6. 스트리밍과 에러 핸들링
6.1) 스트리밍이란?
- 잘게 쪼개진 데이터를 연속적으로 보내주는 기술
- 사용자에게 긴로딩 없이 좋은 경험을 제공
- 일반적인 웹서비스에서도 HTML페이지를 스트리밍 하는 기능 제공
- 단순한 컴포넌트부터 화면에 보여주고 데이터 패칭등으로 렌더링이 오래걸릴 것 같은 컴포넌트를 대체 UI(로딩바)화면에 보여주고 있다가 서버측에서 컴포넌트 렌더링이 완료되면 컨텐츠를 보여준다.
- Static Page보단 Dynamic Page에 자주 사용된다.
loading.tsx
를 생성하면 동일한 경로에 있는 페이지 컴포넌트를 스트리밍 되도록 설정해주면 렌더링하는데 시간이 소요하더라도 전체 페이지의 렌더링을 지연시키지 않는다.- 주의사항
-
- 상위 경로에 있더라도
loading.tsx
로 동일하게 설정된다.
- 상위 경로에 있더라도
-
loading.tsx
파일이 스트리밍 하도록 스트리밍하는 페이지 컴포넌트는async
가 붙어서 비동기로 작동하도록 설정된 페이지 컴포넌트에만 스트리밍을 제공한다.
-
loading.tsx
파일은 Page Component에만 스트리밍을 적용할 수 있다.
- 페이지 컴포넌트(Page.tsx) 외에 별도의 컴포넌트에도 적용하고 싶다면
loading.tsx
가 아니라 react에서 제공하는를 별도로 활용해줘야 한다.
-
loading.tsx
로 설정된 스트리밍은 브라우저에서 query string이 변경될 때는 트리거 되지 않는다.
- 이런 경우에도 스트리밍하고 싶은 경우에는 react에서 제공하는
를 별도로 활용해줘야 한다.
-
6.2) 스트리밍 1. 페이지 스트리밍 적용하기
- 특정 페이지 컴포넌트를 스트리밍 하게 설정하려면 해당 컴포넌트와 동일한 위치에 loading.tsx파일 생성
- loading.tsx에 대체 UI역할을 할 컴포넌트 작성
- loading.tsx는 동일한 경로에 페이지만 스트리밍하는게 아니라 layout파일처럼 해당하는 경로 아래 모든 페이지가 스트리밍 되도록 설정된다.
6.3) 스트리밍 2. 컴포넌트 스트리밍 적용하기
태그로 스트리밍하길 원하는 컴포넌트를 감싸준다. - loading상태를 대체하길 원하는 UI는 Suspense의 fallback이라는 props로 컴포넌트를 전달해준다.(UI자체를 넘겨줘도 ok)
- Suspense를 이용하는 방식 또한 loading.tsx을 이용했던 방법과 동일하게 브라우저에서 쿼리 스트링만 변경되었을 경우에 loading상태가 표시되지 않는다.
- key라는 props를 전달해서 key값이 변화할 때마다 다시 loading상태로 돌아가도록 설정한다.
- Suspense는 최초로 한번 내부 컴포넌트 로딩이 완료된 이후에는 어떠한 컨텐츠가 변경된다 하더라도 기본적으로는 새롭게 로딩상태로 돌아가지 않는다.
- 이럴땐 key값을 줘서 쿼리스트링이 바뀔 때 마다 리액트에게 새로운 컴포넌트로 인식하도록 화면에 새롭게 그리게 설정해준거다.
- 리액트에서는 키값이 바뀌면 컴포넌트가 달라졌다, 다른 컴포넌트가 생겼다 라고 인식한다.
태그를 사용하면 병렬로 하나의 페이지에 여러개의 컴포넌트들을 완료되는 순서대로 각각 렌더링 시킬 수 있다.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{
q?: string
}>
}) {
const { q } = await searchParams
return (
<Suspense key={q || ''} fallback={<div>Loading...</div>}>
<SearchResult q={q || ''} />
</Suspense>
)
}
export default async function Home() {
return (
<div className={style.container}>
<section>
<h3>지금 추천하는 도서</h3>
<Suspense fallback={<BookListSkeleton count={3} />}>
<RecoBooks />
</Suspense>
</section>
<section>
<h3>등록된 모든 도서</h3>
<Suspense
fallback={
<>
<BookListSkeleton count={3} />
</>
}
>
<AllBooks />
</Suspense>
</section>
</div>
)
}
6.4) 스켈레톤 UI적용하기
- 스켈레톤 UI : 컨텐츠가 로드되는 중에 실제 컨텐츠 대신에 보여지게 된다.
- 뼈대역할을 하는 UI
- 실제 컨텐츠 대신에 실루엣만 보여주는 UI
- React Loading Skeleton이라는 라이브러리도 존재 (퍼블리싱이 귀찮으면 해당 라이브러리 사용하자)
6.5) 에러 핸들링
loading.tsx
처럼 error핸들링 할error.tsx
를 해당하는 경로에 생성한다.- 에러 파일과 같은 경로에 있거나 하위 경로에 있는 페이지에서 오류 발생 시 page.tsx대신 화면에 출력된다.
- error.tsx는 클라이언트 컴포넌트로 생성해야 하는데 그 이유는 오류는 서버, 클라이언트 모두 발생할 수 있기 때문에 함께 대응할 수 있어야 하기 때문이다.
- 현재 발생하는 에러나 메시지를 핸들링 하고 싶은 경우에는 props로 전달되는 error를 이용만 하면 된다.
- Next는 에러 발생 시 에러 컴포넌트에게 자바스크립트 error타입의 error객체를 props로 component에 전달해준다.
- props로 reset이라는 props도 전달된다.
- 에러가 발생한 페이지를 복구하기 위해서 다시한번 컴포넌트들을 렌더링 시켜보는 기능을 가진 함수
- 기능을 만들어주더라도 클라이언트 측에서만 다시 렌더링 하기 때문에 서버측에서는 다시실행하지 않아 같은 에러 페이지 노출
- 즉, 클라이언트 내부에서 발생한 오류만 복구
- 서버측에서 실행이 필요한 경우엔 window.location.reload()거나 router.refresh()
- router.refresh()는 현재 페이지에 필요한 서버컴포넌트만 다시 불러옴
- 해당 함수는 비동기적으로 실행되기 때문에 startTransition()을 사용 (async - await는 사용불가 refresh함수는 반환값이 프로미스가 아닌 보이드이기 때문)
- 하나의 콜백함수를 인수로 받아 콜백함수 안에있는 UI변경 작업을 모두 일괄적으로 처리한다.
- 해당 함수는 비동기적으로 실행되기 때문에 startTransition()을 사용 (async - await는 사용불가 refresh함수는 반환값이 프로미스가 아닌 보이드이기 때문)
- reset()함수도 같이 작성하는데 그 이유는 reset()함수가 에러상태를 초기화하고, 컴포넌트를 다시 렌더링하기 때문
- router.refresh()는 현재 페이지에 필요한 서버컴포넌트만 다시 불러옴
'use client'
import { useRouter } from 'next/navigation'
import { startTransition, useEffect } from 'react'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
const router = useRouter()
useEffect(() => {
console.error(error.message)
}, [error])
return (
<div>
<h2>오류가 발생했습니다.</h2>
{/* <button onClick={() => reset()}>다시 시도 </button> */}
{/* <button onClick={() => window.location.reload()}>다시 시도 </button> */}
<button
onClick={() => {
startTransition(() => {
router.refresh()
reset()
})
}}
>
다시 시도{' '}
</button>
</div>
)
}
7. 서버 액션
7.1) 서버 액션이란
- 브라우저에서 호출할 수 있는 서버에서 실행되는 비동기 함수
- 함수 내부에 “use server”를 붙여서 서버측에서만 실행되게 한다.
- 브라우저에서 form에 제출 이벤트가 발생했을 때 서버에서만 실행하는 함수를 브라우저가 직접 호출하면서 데이터까지 form데이터 형식으로 전달할 수 있게 해주는 기능
- 서버액션에서는 매개변수로 formData를 인수로 받는다.
- FormData는 자바스크립트 내장 타입
- 코드상에 서버 액션을 만들게 되면 자동으로 이러한 코드를 실행하는 API가 자동으로 생성된다.
- 이런 API는 브라우저에서 이러한 폼 태그를 제출했을 때 자동으로 호출된다.
- 서버 액션을 사용하는 이유
- client component로 만들던가 별도의 API를 만들어서 호출할 수도 있는데 굳이 사용하는 이유
- 코드가 매우 간결하다
- 이 서버 액션은 오직 서버측에서만 실행되는 코드
- client component로 만들던가 별도의 API를 만들어서 호출할 수도 있는데 굳이 사용하는 이유
function ReviewEditor() {
async function createReviewAction(formData: FormData) {
'use server'
const content = formData.get('content')?.toString()
const author = formData.get('author')?.toString()
}
return (
<div>
<section>
<form action={createReviewAction}>
<input name="content" placeholder="리뷰 내용" />
<input name="author" placeholder="작성자" />
<button type="submit">작성하기</button>
</form>
</section>
</div>
)
}
7.2) 리뷰 추가 기능 구현하기
- action구현 후 분리
<input name="bookId" value={bookId} hidden readOnly/>
readOnly가 없으면 문제 hidden값에는 붙여주는게 좋다.
'use server'
async function createReviewAction(formData: FormData) {
const content = formData.get('content')?.toString()
const author = formData.get('author')?.toString()
if (!content || !author) {
return
}
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/review`,
{
method: 'POST',
body: JSON.stringify({ bookId, content, author }),
},
)
console.log(response.status)
} catch (err) {
console.error(err)
return
}
}
7.3) 리뷰 조회 기능 구현하기
- 페이지를 리렌더링 하고싶을 때
revalidatePath()
사용- next서버가 자동으로 인수로 전달한 경로의 페이지를 재생성
- server action의 결과를 화면에 바로 나타낼 수 있다.
- next서버가 자동으로 인수로 전달한 경로의 페이지를 재생성
- revalidatePath 주의점
-
- revalidatePath는 오직 서버측에서만 호출할 수 있는 메서드이다.
- action내부에서 호출하거나 서버컴포넌트 내부에서만 호출 가능
- client Component에서는 호출 불가능
-
- 경로에 해당하는 페이지를 전부 재검증, 해당 페이지의 모든 캐시를 무효화 시킨다.
- 데이터 캐시는 force-cache로 하더라도 전부 재검증하기 때문에 무효화, 삭제
-
- 풀라우트 캐시까지 함께 삭제(무효화)된다.
- 새롭게 생성된 페이지를 캐시에 저장해주지 않음 -> 새로 접속해야 다이나믹 페이지처럼 생성이 된다.
-
revalidatePath(`/book/${bookId}`)
7.4, 7.5) 리뷰 재검증 구현하기, 다양한 재검증
// 1. 특정 주소의 해당하는 페이지만 검증
revalidatePath(`/book/${bookId}`)
// 2. 특정 경로의 모든 동적 페이지를 재검증
revalidatePath('/book/[id]', 'page')
// 3. 특정 레이아웃을 갖는 모든 페이지 재검증
revalidatePath('/(with-searchbbar)', 'layout')
// 4. 모든 데이터 재검증
revalidatePath('/', 'layout')
// 5. 태그를 기준, 데이터 캐시 재검증
// 태그값을 가지고있는 특정 패치들만 재검증된다.
revalidateTag(`review-${bookId}`)
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/review/book/${bookId}`,
{ next: { tags: [`review-${bookId}`] } },
)
7.6) 클라이언트 컴포넌트에서의 서버 액션
- useActionState() : react19에서 추가된 react hooks
- form 태그의 상태를 쉽게 핸들링할 수 있도록 도와주는 여러가지 기능이 있다.
- 호출하면서 2개의 인수 전달, 첫번째는 핸들링하려는 폼의 액션함수, 두번쨰는 form상태 초기값
- 3개의 값을 반환한다.
- state, action, isPending (현재 실행중인지 아닌지)
- input태그에 hidden으로 사용하고 onChange하지 않을 경우에는 readOnly로 작성해서 수정하지 않는다 명시해야한다
- 보통 input태그에는 onChange가 없으면, 사용자가 입력 필드를 편집할 수 없음
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
import { delay } from '@/util/delay'
export async function createReviewAction(_: any, formData: FormData) {
const bookId = formData.get('bookId')?.toString()
const content = formData.get('content')?.toString()
const author = formData.get('author')?.toString()
if (!bookId || !content || !author) {
return {
status: false,
error: '리뷰 내용과 작성자를 입력해주세요.',
}
}
try {
await delay(2000)
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/review`,
{
method: 'POST',
body: JSON.stringify({ bookId, content, author }),
},
)
if (!response.ok) {
throw new Error(response.statusText)
}
console.log(response.status)
// // 1. 특정 주소의 해당하는 페이지만 검증
// revalidatePath(`/book/${bookId}`);
// // 2. 특정 경로의 모든 동적 페이지를 재검증
// revalidatePath("/book/[id]", "page");
// // 3. 특정 레이아웃을 갖는 모든 페이지 재검증
// revalidatePath("/(with-searchbbar)", "layout");
// // 4. 모든 데이터 재검증
// revalidatePath("/", "layout");
// 5. 태그를 기준, 데이터 캐시 재검증
revalidateTag(`review-${bookId}`)
return {
status: true,
error: '',
}
} catch (err) {
console.error(err)
return {
status: false,
error: '리뷰 작성을 실패했습니다.',
}
}
}
;('use client')
import { useActionState, useEffect } from 'react'
import style from './review-editor.module.css'
import { createReviewAction } from '@/actions/create-review.action'
export default function ReviewEditor({ bookId }: { bookId: string }) {
const [state, formAction, isPending] = useActionState(
createReviewAction,
null,
)
useEffect(() => {
if (state && !state.status) {
alert(state.error)
}
}, [state])
return (
<div>
<section>
<form className={style.form_container} action={formAction}>
<input name="bookId" value={bookId} hidden readOnly />
<textarea
disabled={isPending}
required
name="content"
placeholder="리뷰 내용"
/>
<div className={style.submit_container}>
<input
disabled={isPending}
required
name="author"
placeholder="작성자"
/>
<button disabled={isPending} type="submit">
{isPending ? '...' : '작성하기'}
</button>
</div>
</form>
</section>
</div>
)
}
7.7) 리뷰 삭제 기능 구현하기
- submit()메서드 보단 requestSubmit()을 추천
- submit()메서드는 유효성 검사나 이벤트 핸들러들을 전부 다 무시하고 강제로 form제출을 발생시킨다. (주의해서 사용)
- requestSubmit()이라는 메서드는 실제로 사용자가 submit 버튼을 클릭한거와 동일하게 비교적 의도한대로 안전하게 동작
8. 고급 라우팅 패턴
8.1) 페러렐 라우트
- Parallel Route : 병렬 라우트
- 하나의 화면안에 여러개의 페이지(Page.tsx)를 병렬로 함께 렌더링
- ex) sns, 관리자 페이지
- 하나의 화면안에 여러개의 페이지(Page.tsx)를 병렬로 함께 렌더링
- 이름앞에 골뱅이 표시가 붙은 폴더를
슬롯
이라 한다.- 슬롯이 하는 역할은 병렬로 렌더링 될 하나의 페이지 컴포넌트를 보관하는 폴더
- 슬롯안에 보관된 페이지 컴포넌트는 부모 레이아웃 컴포넌트의 props로 자동으로 전달된다.
- Slot(슬롯)은 URL 경로에는 아무런 영향을 미치지 않는다.
- Slot은 몇개가 되도 상관이 없다.
- 하위 폴더에 폴더를 만들고 페이지를 만들면 layout에 속해 하위 페이지로 생성된다.
- 해당 컴포넌트만 교체되는 형식
- 이렇게 이전의 페이지가 유지가 되는 건
Link컴포넌트
를 이용해서 브라우저측에서 클라이언트 렌더링 방식으로 이동할 때만 한정(새로고침하면 404에러)- 방지하기 위해선 슬롯별로 현재 렌더링 하는 페이지가 없을 때 대신 렌더링 할 디폴트 페이지를 만들면 된다.
- Layout에 제공되는 Children페이지 또한 @Children안에 있는 페이지 컴포넌트라 봐도 똑같다.
- 매번 만들면 귀찮을 수도 있기 때문에 슬롯을 만들지 않고 일반적인 페이지 봐도 무방
8.2) 인터셉팅 라우트
- 사용자 요청을 가로채서 원래 렌더링 되야하는 페이지가 아닌 원하는 어떤 페이지를 대신 렌더링하도록 설정하는 라우팅 패턴
- 초기설정이 아닐 경우에만 인터셉팅 라우트가 동작
- Link컴포넌트나 Router에서 제공하는 push같은 클라이언트 사이드 방식으로 페이지를 이동할 때만 인터셉팅이 일어난다.
- ex) 인스타그램 - 상세 페이지 누르면 모달형태(client rendering / 인터셉팅 라우트), 새로고침 시 해당 페이지로 이동
(.)book[id]
같은 형태로 작성- (.)동일한 레벨 의 세그먼트를 일치시키려면
- (..)한 단계 위의 세그먼트와 일치시키려면
- (..)(..)두 레벨 위의 세그먼트와 일치시키려면
- (…)루트 app 디렉토리 의 세그먼트와 일치시키려면
- modal구현
- createPortal() 사용
- 브라우저에 존재하는 DOM요소 아래에 고정적으로 Modal요소를 렌더링 하도록 설정
- createPortal() 사용
8.3) 패럴랠 & 인터셉팅 라우트
- 모달 뒷부분에 index페이지를 노출하고 싶은 경우
- slot활용
9. 최적화와 배포
9.1) 이미지 최적화
- Next에서는 자체적으로 이미지 최적화가 적용되어 있다.
- webp, AVIF등의 차세대 형식으로 변환하기 (jpeg같은 포맷 대신)
- 디바이스 사이즈에 맞는 이미지 불러오기
- image의 width와 height를 명시해줘야 하는데 이유는 필요이상으로 큰 이미지를 불러오지 않을 수 있도록 설정
- 레이지 로딩 적용
- 블러 이미지 활용
- 기타 등등
- 설정을 해주면 에러가 나는데
next.config.js
에 특정 경로들은 안전하다고 선언해줘야 한다.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
experimental: {
typedRoutes: false, // ← 이걸 통해 타입 충돌 완화 가능
// fetch instrumentation은 아직 공식 도큐먼트엔 없음
},
logging: {
fetches: {
fullUrl: true,
}
},
images: {
domains: ["shopping-phinf.pstatic.net"]
}
};
export default nextConfig;
9.2) 검색 엔진 최적화(SEO)
- 검색엔진 최적화(SEO)
- sitemap설정하기
- rss발행하기
- 시멘틱 태그 설정하기
- 메타 데이터 설정하기
- index페이지에서 metadata라는 변수를 선언해서 export로 내보내주면 metadata로 설정된 값이 자동으로 index페이지의 metadata로 설정된다.
- slash뒤에 정적인 image file을 적어주면 public directory아래를 가르키게 된다.
- 동적인 값을 활용해서 메타데이터를 설정하는 경우엔 generateMetadata()로 메타데이터를 내보낸다.
- 매개변수를 전달받을 수 있어서 페이지 컴포넌트와 동일하게 값을 받아온다.
export const metadata: Metadata = {}
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}): Promise<Metadata> {
const { q } = await searchParams
return {
title: `${q} : 한입북스 검색`,
}
}
Tip!!
- react에서 일반 변수와 state 사용 차이점
- 일반 변수는 값만 저장됨
- useState로 만든 state는 값 + 자동 리렌더링이 같이 작동