Devrank 프로젝트에서 Next.js를 처음 사용한 이후로 최근 Next.js에 관심이 많아졌다. Next.js app router도 최근 stable이 되었고 지금도 앞으로도 Next.js는 trend가 될 것 같다.
이 글은 Next.js 공식문서의 pages router 중 핵심 내용을 선별해 정리한 글이다.
pages
디렉토리에 존재하는 리액트 컴포넌트 파일에 해당한다. 각 페이지는 파일명에 따라 라우팅 경로와 연관된다.[]
를 통해 적용 가능. 예를 들어 pages/posts/[id].js 파일이 존재할 시 posts/1 ~ posts/n 페이지에서 접근가능하다.기본적으로 Next.js는 모든 페이지를 pre-rendering 한다. Next.js는 각 페이지를 모두 클라이언트측 자바스크립트에서 생성하는 대신 미리 HTML 파일을 만든다.
결국 완성된 HTML 문서를 전송하기 때문에 성능 효율적이며 SEO에 유리하다.
각 HTML파일은 최소한의 자바스크립트 코드만이 필요하다. 브라우저에 페이지가 로드되면 자바스크립트가 로드되어 페이지가 완전히 동작가능하게 되는데 이 과정을 hydration
이라 한다.
Next.js는 기본적으로 SSG(Static Site Generation)와 SSR(Server-side Rendering) 두 가지 형태의 pre-rendering을 지원한다.
Next.js 측에선 SSR을 꼭 사용해야하는 상황이 아니라면 SSG를 SSR보다 적극 사용하길 추천한다. SSG는 CDN에 캐싱될 수 있기 때문에 성능효율적이다.
이외에도 Client-side data fetching 또한 가능하다. 즉, SSR, SSG, CSR 중 필요에 따라 선택적으로 사용하면 된다.
만약 페이지가 SSG를 사용하면 HTML 문서는 빌드타임에 미리 생성된다. (next build
를 실행시킬 때) 해당 문서는 매 요청마다 재사용된다.
data fetching이 없는 페이지일 시 Next.js는 default로 페이지를 SSG로 pre-rendering 한다.
function Page() {
return <div>page</div>
}
export default Page;
페이지를 pre-rendering하기 위해 외부 데이터를 fetching해야 할 경우는 두가지 케이스가 존재한다.
getStaticProps
getStaticPaths
, getStaticProps
또한 사용가능페이지를 pre-render할 때 필요한 외부 데이터를 fetch 해야 하는 경우 async 함수인 getStaticProps
를 같은 파일 내에서 export해야 한다. 해당 함수는 빌드 타임에 호출되고 pre-render시에 fetch된 데이터를 페이지의 props로 넘길 수 있게 해준다.
// props로 getStaticProps에서 return한 Props정보를 전달받아 사용할 수 있다.
export default function Page({data}) {
//... some code
}
// 빌드타임에 호출
export async function getStaticProps() {
const res = await fetch(요청경로);
const data = res.json();
return {
props: {
data,
},
}
}
페이지의 path가 외부 데이터에 의존할 경우 getStaticPaths
를 고려할 수 있다. Next.js는 dynamic route를 지원하나 어떤 dynamic값을 가지는 페이지를 빌드타임에 pre-render할 지는 외부 데이터에 의존할 수 있다.
export async function getStaticPaths() {
const res = await fetch(요청경로);
const items = res.json();
// pre-render할 path 정보를 획득한다.
const paths = items.map((item) => ({
params: { id: item.id },
));
// paths에 해당하는 문서만 빌드타임에 pre-render
// fallback이 false일 경우 다른 라우트 경로는 404가 된다.
return { paths, fallback: false };
}
// 데이터를 fetching 해야할 경우 getStaticPaths에서 전달받은 path params를 사용할 수 있다.
export async function getStaticProps({params}) {
const res = await fetch(`경로/${params.id}`);
const item = await res.json();
return {
props: { item }
};
}
export default function Item({item}) {
// some code
}
가능한 한 사용하길 권장한다. 페이지가 한번만 만들어지고 CDN에 캐싱되어 매 요청마다 생성하는 것에 대비해 매우 빠르다.
하지만 페이지의 content가 매 요청마다 갱신되어야 한다면 문서를 미리 생성할 수 없기 때문에 SSG를 사용할 수 없다. → CSR, SSR을 사용해야 함
SSR을 사용하면 페이지는 각 요청마다 새로 생성된다. SSR을 사용하기 위해선 async function인 getServerSideProps
를 export 해야한다. 해당 함수는 매 요청마다 호출된다.
만약 페이지의 데이터가 요청마다 빈번하게 갱신되어야 하면 사용할 수 있다.
export default function Item({item}) {
// some code
}
export async function getServerSideProps() {
const res = await fetch(경로);
const item = await res.json();
return {
props: { item }
};
}
getServerSideProps
함수를 export하면 Next.js는 매 요청마다 함수에서 반환된 데이터를 사용해 페이지를 pre-render한다.
// 페이지가 SSR로 동작하게 된다.
export async function getServerSideProps(context) {
return {
props: {}, // 페이지 컴포넌트에 props로 전달
}
}
getServerSideProps
는 항상 브라우저가 아닌 server-side에서만 실행된다.
getServerSideProps
가 요청 시간에 실행되고 페이지는 반환된 props와 함께 pre-render된다.next/link(Link)
나 next/router(useRouter)
를 통해 클라이언트 측 페이지 이동간에 요청된다면 Next.js는 서버에 getServerSideProps
를 실행시키는 API 요청을 전달한다.getServerSideProps
는 페이지를 렌더링하는데 사용할 수 있는 JSON 데이터를 반환한다.getServerSideProps
는 페이지 컴포넌트에서만 사용가능하고 단독으로 export해야 한다.getServerSideProps
를 사용해야 한다.getStaticProps
함수를 export하면 Next.js는 빌드타임에 반환된 데이터를 사용해 페이지를 pre-render한다.
// 페이지가 SSG로 동작한다.
export async function getStaticProps(context) {
return {
props: {}, // props로 전달
}
}
getStaticProps
는 항상 서버에서 실행되고 클라이언트에서는 절대 실행되지 않는다.
next build
중에 실행된다.getStaticProps
로 빌드타임에 생성된 페이지는 HTML파일만이 아닌 함수 반환값으로 JSON파일 또한 정적으로 생성한다. JSON파일은 client-side에서 routing간에 페이지를 render할 때 필요한 props값을 주입하기 위해 사용된다.
즉, getStaticProps
가 호출되는 것이 아닌 생성되었던 JSON파일을 재사용한다.
페이지가 dynamic route의 형태를 가지면서 getStaticProps
를 사용한다면 여러개의 path를 가지는 문서를 정적으로 생성할 필요가 있다.
getStaticPaths
함수를 export하면 Next.js는 getStaticPaths
에서 특정된 모든 path에 대한 문서를 pre-render한다.
// pages/items/[id].js
// items/1과 items/2 를 생성한다.
export async function getStaticPaths() {
return {
// 각 path에 대한 정보를 전달한다.
paths: [{params: {id: '1' }}, {params: {id: '2' }}],
fallback: false,
}
}
// getStaticPaths는 getStaticProps를 함께 호출해야 한다.
export async function getStaticProps(context) {
//
}
ISR을 사용하면 빌드 후에도 전체 사이트를 rebuild하는 대신 페이지 별로 정적인 페이지를 갱신할 수 있다.
ISR을 사용하려면 revalidate
prop을 getStaticProps
에 추가한다.
export async function getStaticProps() {
// 코드
return {
props: { data },
// 매 10초 마다 페이지 갱신
// 요청이 올 시 페이지 갱신
revalidate: 10, // 초 단위
}
}
페이지가 빌드 타임에 생성되고 첫 요청은 cache된 페이지를 보여준다. 이후로는 다음과 같다.
revalidate
시간을 60으로 하면 모든 사용자들은 1분동안 같은 화면을 보게되고 cache를 무효화하는 유일한 방법은 1분뒤 누군가가 페이지를 방문하는 것이다.
이러한 이유로 Next.js는 On-demand ISR을 제공한다. on-demand revalidation을 하기위해서는 getStaticProps
에서 revalidate
를 명시하지 않는다. 만약 페이지 캐시를 무효화하기 위해서는 revalidate()
함수를 호출하면 된다.
사용법은 공식문서를 참고 → https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration
다음 경우에 client-side data fetching이 유용하다.
Next.js에선 네비게이션바(topbar)나 푸터와 같은 페이지마다 동일한 UI는 layout으로 작성해서 페이지마다 새로 작성하지 않고 공유해서 사용할 수 있는 구조를 제시한다.
전체 페이지에 하나의 레이아웃 구조만이 필요하면 Layout 커스텀 컴포넌트를 작성하고 _app.js
에서 페이지 컴포넌트를 감싸서 재사용할 수 있다. 페이지 컴포넌트가 변화해도 레이아웃은 변화하지 않기 때문에 컴포넌트 state가 유지되는 이점이 있다.
//components/Layout.js
// Layout으로 사용할 컴포넌트를 정의
// children이 페이지 컴포넌트가 된다.
export default function Layout({children}) {
return (
<>
<nav></nav>
<main>{children}</main>
<footer></footer>
</>
);
}
//pages/_app.js
export default function MyApp({ Component, pageProps }) {
return (
// Layout 컴포넌트로 감싸준다.
<Layout>
<Component {...pageProps} />
</Layout>
)
}
만약 여러개의 레이아웃이 필요하면 페이지에 getLayout 프로퍼티를 추가해서 페이지별 레이아웃을 정의할 수 있다.
export default function Page() {
// JSX return
}
Page.getLayout = function getLayout(page) {
// page를 layout nesting
}
// pages/_app.js
export default function MyApp({ Component, pageProps }) {
// component에 getLayout 프로퍼티가 있으면 getLayout으로 페이지 별 레이아웃 적용
// 없으면 컴포넌트를 그대로 반환하는 함수 할당
const getLayout = Component.getLayout || ((page) => page)
return getLayout(<Component {...pageProps} />)
}
Layout 컴포넌트는 페이지가 아니기 때문에 client side data fetching만이 가능하다.
Next.js에서 제공하는 next/image
컴포넌트는 <img>
태그의 확장으로 기존 img태그를 성능을 최적화한 컴포넌트이다.
import Image from 'next/image';
다음의 최적화 기능을 제공한다.
각 페이지의 LCP(viewport에 존재하는 가장 큰 이미지나 텍스트 블록)가 될 이미지에는 priority
속성을 추가해서 Next.js가 해당 이미지를 우선 로드할 수 있게 해주어야 한다.
// LCP image
<Image
// 속성들
// priority를 추가
priority
/>
이미지에 사이즈를 지정해주지 않으면 이미지가 로드되기 전 비어있던 공간이 이미지가 로드되면서 화면이 밀리는 현상이 생기는데 이는 성능에 큰 저하를 일으킨다. → (CLS, Cumulative Layout Shift)
next/image
는 이를 다음의 3가지 방법으로 방지한다.
fill
property를 추가하면 이미지가 parent element에 따라 동적으로 확장된다.fill
사용 fill
속성은 이미지 사이즈가 부모 element에 따라 변화할 수 있게 해준다.더 자세한 건 API 문서를 보자. https://nextjs.org/docs/api-reference/next/image
next/font
는 폰트를 최적화해주고 외부 네트워크 요청을 제거해준다.
next/font/google
을 통해 font import 가능// 아무 파일에서든 가능
// _app.js에서 추가해준다면 하위 컴포넌트 전체에 적용
import { Roboto } from 'next/font/google'
const roboto = Roboto({
weight: ['400', '700'], // weight 지정가능
style: ['normal', 'italic'], // style 지정가능
subsets: ['latin'],
})
export default function MyApp({ Component, pageProps }) {
return (
<main className={roboto.className}>
<Component {...pageProps} />
</main>
)
}
next/font/local
를 사용하면 local font 또한 향상된 성능으로 사용할 수 있다.
import localFont from 'next/font/local'
// local font src를 전달
const myFont = localFont({ src: './my-font.woff2' })
export default function MyApp({ Component, pageProps }) {
return (
<main className={myFont.className}>
<Component {...pageProps} />
</main>
)
}
💡 Reusing font
폰트를 호출할 때마다 새로운 인스턴스로 호스팅되기 때문에 만약 여러 파일에서 동일한 폰트를 사용해야 하면 하나의 파일에서 폰트 로더를 모듈화시켜 필요한 파일에서 import 해서 사용해야 한다.
Next.js는 루트 디렉토리의 public
폴더에서 정적 asset들을 제공한다. public
폴더 아래의 파일들은 코드에서 /
를 base-URL로 접근가능하다.
Next.js는 환경변수를 세팅할 수 있는 방법을 내장으로 지원한다.
.env.local
사용NEXT_PUBLIC_
prefix 환경변수 사용(브라우저에서 사용하려면)Next.js는 .env.local
에 있는 환경변수들을 자동으로 process.env
에 추가해준다.
variable 또한 사용 가능
HOSTNAME=localhost
PORT=8080
// $(variable) 변수로 사용
HOST=http://$HOSTNAME:$PORT
기본적으로 환경변수는 Node.js 환경에서만 사용가능하기 때문에 브라우저에 노출되지 않는다. 브라우저에 노출시키기 위해서는 환경변수에 NEXT_PUBLIC_
prefix를 붙여야 한다.
만약 환경변수를 production과 development, test 환경에 따라 나누어 적용하고 싶으면 .env.production
, .env.development
, .env.test
등으로 나누어 생성하면 된다.
third-party scripts는 사용자나 개발자의 경험을 떨어뜨릴 수 있다.
일반 <script>
태그는 몇가지 문제가 있다.
Script 컴포넌트는 이러한 문제를 몇가지 로딩 전략을 통해 최적화한다.
import Script from 'next/script'
export default function Page() {
return (
<>
<Script src="https://example.com/script.js" />
</>
)
}
next/script
는 strategy
property를 통해 로딩 동작을 조정할 수 있다.
beforeInteractive
: Next.js 코드와 페이지가 하이드레이션 되기 전에 스크립트를 로드한다.afterInteractive(default)
: 하이드레이션이 페이지에 일어나고난 직후에 스크립트를 로드한다.lazyOnload
: 브라우저 idle time에 스크립트를 lazy load한다.worker(experimental)
: script 로드를 web worker에 맡긴다.몇가지 event handler 속성으로 스크립트 컴포넌트 동작 이후 추가적인 코드를 실행할 수 있다.
onLoad
: script가 로드되고 난 후 동작onReady
: script 로드되고 난 후와 컴포넌트가 마운트될 때마다onError
: 로드에 실패했을 때import Script from 'next/script'
export default function Page() {
return (
<>
<Script
src="https://example.com/script.js"
onLoad={() => {
console.log('Script has loaded')
}}
/>
</>
)
}
Next.js는 파일 시스템 기반 라우트 구조를 사용한다. pages
디렉토리에 추가된 파일들은 자동으로 접근 가능한 경로가 된다.
index
파일은 디렉토리 루트 경로에 해당한다.pages/index.js
→ /
pages/blog/index.js
→ `pages/board/settings.js
→ /board/settings
[](bracket)
을 통해 dynamic route 또한 가능하다.pages/item/[id].js
→ /item/:id
pages/item/[...all].js
→ /item/*
*뒤로 어떠한 경로가 오든 match한다.Next.js router는 Link
컴포넌트를 통해 페이지 간 이동에 SPA와 같은 client-side route 이동이 가능하게 한다.
Link
컴포넌트는 viewport에 노출되었을 시 SSG 페이지를 prefetch 한다. SSR 페이지의 경우 클릭 시 페이지 데이터를 fetch한다.
dynamic route시 경로에 포함되는 dynamic parameter는 페이지에 query parameter로 전달되어 사용할 수 있다.
import { useRouter } from 'next/router'
const Post = () => {
const router = useRouter()
const { pid } = router.query
return <p>Post: {pid}</p>
}
export default Post
[](bracket)
사이에 ...
을 추가하면 모든 경로를 커버할 수 있다.
pages/items/[...id].js
→ /items/*.js
에 해당items/a/b
→ { “id”: [”a”, “b”] }
전달[[...all]]
catch all route를 이중 bracket으로 사용하면 optional catch all route가 된다.
/items
또한 커버한다.client-side navigation은 링크 컴포넌트가 아닌 next/router
를 통해서도 가능하다.
import { useRouter } from 'next/router'
export default function ReadMore() {
const router = useRouter()
return (
// router.push()에 전달한 경로로 이동
<button onClick={() => router.push('/about')}>
Click here to read more
</button>
)
}
Shallow routing은 data fetching method(getServerSideProps
, getStaticProps
, getInitialProps
)를 재실행하지 않고 URL을 변경할 수 있게 해준다. 기존의 상태를 유지하면서 URL만 변경하고 싶을 때 사용하면 될 것 같다.
사용법은 router에 shallow
옵션을 true로 전달하면 된다.
import { useEffect } from 'react'
import { useRouter } from 'next/router'
export default function Page() {
const router = useRouter()
useEffect(() => {
// 세번째 인자에 shallow option을 true로 전달
router.push('/?q=asdf', undefined, { shallow: true })
}, [])
useEffect(() => {
// q 쿼리 파라미터가 변경되었을 때
}, [router.query.q])
}
위의 경우 URL은 변경되지만 페이지는 갱신되지 않는다. 즉, route 상태만 변경되었다.
💡 Noteshallow routing은 같은 페이지의 URL 변경에 대해서만 작동한다. 예를 들어 About 페이지가 있는데 다음과 같이 about페이지의 경로로 URL을 변경하면 About페이지에 대한 경로이기 때문에 shallow option을 주었어도 새 페이지로 변경하고 data fetching을 수행한다.
router.push('/?count=10', '/about?counter=10', {shallow:true})
💡 shallow routing은 페이지 리렌더링을 야기한다?
router.push든 router.replace든 일어나면 페이지가 리렌더링 된다고 한다. 이는 next/router
가 Context API를 내부적으로 사용하기 때문에 router.*
를 실행하면 내부 상태 값이 변경되고 이는 리렌더링을 야기한다고 한다. (근데 왜 공식문서에 The URL will get updated to /?counter=10and the page won't get replaced
이렇게 되어있지…?)
해결 방법은 window.history.replaceState
를 사용하는 것이다.
window.history.replaceState(
window.history.state,
'',
window.location.pathname + '?' + 'whatever=u_want',
)
https://yceffort.kr/2021/12/nextjs-lesson-and-learn
API routes는 Next.js에서 serverless API 엔드포인트를 만들수 있게 해준다.
pages/api
내의 파일들은 페이지가 아닌 API 엔드포인트로 /api/*
경로로 매칭된다. 이 파일들은 server 측에서만 번들되기 때문에 클라이언트 측 번들크기에는 영향이 없다.
API route를 사용하기 위해서 파일 내에서 request handler함수를 export default 해야 한다. request handler는 다음의 두 파라미터를 전달받는다.
http.IncomingMessage
의 인스턴스로 요청에 대한 객체http.ServerResponse
의 인스턴스로 응답에 대한 객체// pages/api/user.js
// api/user의 요청에 대해 200 status code의 JSON 데이터를 응답한다.
export default function handler(req, res) {
res.status(200).json({name: 'John Doe'});
}
API route에서 HTTP 메소드 각각에 대해서 다루기 위해서는 req.method
를 사용할 수 있다.
export default function handler(req, res) {
// req.method에 따라 분기를 만든다.
if (req.method === 'POST') {
// POST 요청에 따른 작업
} else {
// 다른 HTTP 메소드
}
}
/api/secret
을 https://company.com/secret-url
대신사용API routes 또한 pages와 같이 Dynamic routes를 지원한다. 예를 들어 pages/api/item/[id].js
의 경우 req.query
를 통해서 dynamic parameter를 사용할 수 있다.
// 요청 시 id값을 응답한다.
export default function handler(req, res) {
const { id } = req.query;
res.end(id);
}
index route를 처리하는 패턴으로 두 가지가 있다.
/api/items.js
: index route/api/items/[id].js
: dynamic route/api/items/index.js
: index route/api/items/[id].js
: dynamic routepages와 같이 catch all route([…all]
) 과 optional catch all route([[…all]]
) 둘 다 사용 가능하다. 라우트 우선순위 또한 pages와 동일하다.
API routes는 요청(req)을 해석하는 내장 request helpers(미들웨어)를 제공한다.
req.cookies
요청에 포함된 cookie값을 포함하는 객체req.query
query string정보를 포함하는 객체req.body
body가 전달되지 않았을 땐 null or body 정보를 포함하는 객체모든 API Route는 config
객체를 export 할 수 있다. config를 사용하면 기본 설정을 변경할 수 있다.
export const config = {
// 이런식으로 기본설정 custom 가능
api: {
bodyParser: {
sizeLimit: '1mb'
}
}
}
api
객체에 모든 config option을 설정할 수 있다.bodyParser
는 기본 enabled상태. body를 Stream
이나 raw-body
로 사용하고 싶으면 false로 변경하면 된다.bodyParser.sizeLimit
는 parsed body의 최대 크기(byte단위)를 설정한다.externalResolver
는 경로가 외부 resolver에 의해 처리되고 있음을 알리는 플래그로 이 옵션을 사용하면 확인되지 않은 요청에 대한 경고가 비활성화된다.responseLimit
는 기본 enabled 상태(응답 body 크기가 4MB를 넘으면 경고)response 객체는 Express와 유사한 method set을 제공한다.
res.status()
응답코드 정의res.json()
JSON 데이터 전송res.send()
HTTP응답 전송 string
or object
or Buffer
res.redirect()
Redirect 수행res.revalidate()
SSG 페이지를 재생성한다.export default async function handler(req, res) {
// 일반적으로 하는 방식이랑 똑같이 사용할 수 있다
try {
const result = await requestSomething();
res.status(200).send({ result })
} catch (err) {
res.status(500).send({ error: 'error occurred' })
}
}
handler를 type-safe하게 사용할려면 NextApiRequest
와 NextApiResponse
타입을 사용할 수 있다.
import type { NextApiRequest, NextApiResponse } from 'next'
type ResponseData = {
// 응답 데이터 type
}
export default function handler(
req: NextApiRequest,
// 응답 데이터 type을 generic으로 전
res: NextApiResponse<ResponseData>
) {
res.status(200).json(응답데이터)
}
💡 Note
NextAPIRequest
의 body는 클라이언트가 어떤 데이터 타입의 페이로드를 전달할지 모르니 any
타입이다. 따라서 런타임에 타입을 검사해서 사용해야 한다.
Edge API routes를 사용하면 기존 Node.js 기반 런타임보다 가볍고 빠른 환경을 제공한다. edge runtime을 활성화 시키고 싶으면 라우트에서 runtime config를 export 하면 된다.
export const config = {
runtime: 'edge',
}
export default function handler(req) {
// edge runtime에서 지원하는 API인 Response로 응답하는듯?
return new Response('Hello world!')
}
Edge API routes는 Edge Runtime을 사용하고 API routes는 노드js 런타임을 사용한다.
Edge API routes는 표준 web API를 기반으로 만들어졌기 때문에 몇 가지 제약사항이 존재하나 서버에서 응답을 스트리밍하고 캐시된 파일에 접근한 후 실행된다. 서버 측 스트리밍은 빠른 TTFB(Time To First Byte)로 성능을 향상시킬 수 있다.
💡 Edge RuntimeEdge runtime에서 지원하는 API와 지원하지 않는 API는 공식문서에서 확인해 볼 수 있다.
https://nextjs.org/docs/api-reference/edge-runtime
Next.js는 여러개의 인증 패턴(use case가 각각 다름)을 지원한다.
https://nextjs.org/docs/authentication 코드는 여기서 보자
next.js는 getServerSideProps
나 getInitialProps
가 존재하지 않으면 페이지를 자동으로 정적이라고 판단한다. 대신 페이지가 서버측에서 로딩상태를 렌더링하고 클라이언트 측에선 데이터를 가져올 수 있다.
이러한 패턴은 페이지가 CDN으로부터 제공되고 next/link
에 의해 preload될 수 있어 TTI가 빨라진다.
만약 getServerSideProps
를 사용하는 SSR 페이지일 경우 getServerSideProps
에서 유저 데이터를 props로 페이지에 전달해주어 로딩상태를 표시하지않고 바로 콘텐츠를 표시할 수 있다. 이 때 인증과정으로 인해 렌더링이 지연될 수 있으니 인증 조회가 빠른지 확인하고 지연이 발생할 시 SSG를 고려해보아야 한다.
Next.js는 App
컴포넌트를 사용하여 페이지를 초기화한다. App을 커스텀하면 다음의 것들이 가능하다.
App을 커스텀하려면 /pages/_app.js
를 작성한다.
// Component prop = 활성화된 페이지 컴포넌트
// pageProps = data fetching을 통해 preload된 props 객체
export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
커스텀 Document
를 통해 <html>
과 <body>
태그를 수정할 수 있다. 다큐먼트 파일은 서버에서만 렌더링 되기 때문에 이벤트 핸들러는 사용이 불가능하다.
Document
를 override하기 위해서 pages/_document.js
파일을 생성한다.
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
// 여기서 custom한다.
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
💡 Note
<Head />
컴포넌트는 next/head
와 별개이다. <Head />
는 모든 페이지에 공통인 head태그만을 조작할 수 있다. 다른 경우에는 각 페이지에서 next/head
를 사용하는것이 옳다.<Main />
바깥의 리액트 컴포넌트는 브라우저에서 초기화되지 않으므로 로직이나 CSS를 추가해선 안된다.Next.js는 404페이지를 기본적으로 정적생성해 제공한다. 만약 커스텀 404페이지를 제공하고 싶다면 pages폴더 아래에 404.js
파일을 생성해서 커스텀할 수 있다.
500 페이지도 동일하다. 만약 커스텀 500페이지를 생성하고 싶다면 500.js
파일을 생성하면 된다.
500 에러의 경우 클라이언트와 서버에서 Error
컴포넌트에 의해 다뤄진다. Error
컴포넌트를 override할려면 _error.js
파일을 생성해 커스텀하면 된다.
Middleware는 요청이 완료되기 전에 특정 코드를 실행할 수 있게 해준다. 이를 통해 rewrite, redirect, 요청 응답 헤더 수정 등의 작업을 할 수 있다.
일단 쓸려면 next latest버전을 설치해야 한다.
npm install next@latest
middleware.ts
파일을 pages와 같은 레벨에 위치하게 생성한다.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// 비동기 처리를 해야 하면 async 함수로 쓰자
export function middleware(request: NextRequest) {
// redirect 처리 등등 추가적인 동작 실행 가능
return NextResponse.redirect(new URL('/about-2', request.url))
}
// 일치하는 경로에 대한 설정
export const config = {
matcher: '/about/:path*',
}
미들웨어는 모든 라우트에서 실행된다.
1. next.config.js의 `headers`, `redirects`
2. Middleware
3. nextjs.config.js의 `beforeFiles`
4. 파일 시스템의 모든 파일 (`public`, `_next/static/, 페이지들)
5. nextjs.config.js의 `afterFiles`
6. Dynamic 라우트들 (ex: `/card/[cardId]`)
7. nextjs.config.js의 `fallback`
미들웨어를 실행할 경로를 특정하기 위해서 다음의 두 방법이 있다.
matcher를 통해 특정한 경로에서 미들웨어가 실행되게 필터링할 수 있다.
export const config = {
// 배열로 여러개의 경로도 설정 가능
// matcher: ['/about/:path*', '/dashboard/:path*'],
// 정규표현식도 사용가능
// matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)'
matcher: '매칭할 경로'
}
/
로 시작해야 한다./about/:path
와 같이 parameter를 가질 수 있다. path 값은 동적인 값/about/:path*
처럼 *
를 파라미터 뒤에 붙이면 0개 이상의 sub path가 올 수 있다. ?
는 0 or 1, +
는 1개이상()
안에 삽입해서 사용가능하다. Ex) /about/(.*)
export function middleware(request: NextRequest) {
// 내부에서 조건문으로 분기처리 하는 방법
if (request.nextUrl.pathname.startsWith('/about')) {
return NextResponse.rewrite(new URL('/about-2', request.url))
}
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
NextResponse
API 를 통해 다음의 동작을 수행할 수 있다.
미들웨어에서 response하기 위한 방법 두가지
이외에도 여러가지 처리가 가능함.
쿠키 처리
요청, 응답 객체에 cookies
객체를 통해 편리하게 쿠키 처리 가능
헤더
NextResponse
API를 통해 응답/요청 헤더 설정 가능
middleware flag
skipMiddlewareUrlNormalize
와 skipTrailingSlashRedirect
두 가지 플래그를 사용해 고급 처리를 할 수 있다.
skipTrailingSlashRedirect
는 URL 마지막 슬래시 추가 or 제거에 대한 Next.js의 기본 리다이렉트 기능을 끄고 미들웨어 내에서 사용자 정의 처리를 할 수 있게 한다. 일부 경로에서 마지막 슬래시를 유지하고 다른 경로에선 제거할 수 있어 점진적 마이그레이션을 쉽게 할 수 있다.skipMiddlewareUrlNormalize
는 클라이언트 이동 및 직접 방문을 처리할 때 Next.js가 수행하는 URL 정규화를 비활성화하여 원본 URL을 사용해 전체 처리가 필요할 때 사용 (언제 쓰지?.. 감이 안잡힌다..)개발환경에서 런타임 오류 발생 시 Next.js는 오류 오버레이 화면을 띄운다.(only development에서만)
라잌 디스
일단 Next.js가 기본적으로 정적 페이지인 500페이지를 제공하는데 pages/500.js
파일 커스텀해서 작성하면 이걸로 뜬다.
클라이언트 측 오류를 처리할 때 React Error boundary를 사용하길 추천하고 있다. Error boundary쓰면 페이지가 터지지 않고 custom fallback UI를 보여주며 에러 로그도 찍을 수 있으니 꿀이라고 한다.
Error boundary
쓸려면 클래스 컴포넌트로 작성해야 한다고 한다. 그리고 만든걸로 pages/_app.js
의 페이지 컴포넌트를 감싸주자.
어플리케이션 보안을 향상할려면 모든 라우트에 HTTP 응답헤더를 적용하기 위해 next.config.js
에 headers
설정을 사용하라고 한다.
// next.config.js
// 뭐 이런식임
// 여기 사용할 헤더 추가
const securityHeaders = []
module.exports = {
async headers() {
return [
{
// source가 헤더를 적용할 라우트
// 이렇게하면 모든 route에 적용된다.
source: '/:path*',
headers: securityHeaders,
},
]
},
}
DNS prefetching을 제어해서 브라우저가 외부링크, 이미지, CSS, JS 등에서 도메인 명 확인을 사전에 할 수 있게 한다. 백그라운드에서 수행되므로 참조된 항목이 필요할 때까지 DNS가 해결될 가능성이 높아서 링크 클릭시 지연이 줄어든다 꿀👍
{
key: 'X-DNS-Prefetch-Control',
value: 'on'
}
HTTP 대신 HTTPS를 사용하여 접근해야 함을 브라우저에 알린다. 이렇게 하면 HTTP를 통해서만 제공될 수 있는 페이지 또는 하위 도메인에 대한 접근이 차단된다. 참고로 Vercel에 배포하면 이거 설정안해도 알아서 다해준다고 한다.
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload' // max age 2년동안 HTTPS 사용, sub domain 포함
}
페이지가 XSS 공격을 탐지하면 페이지 로드를 중지한다. CSP를 사용하면 인라인 자바스크립트 사용을 금지해 안써도되는데 CSP 지원안하는 웹 브라우저에 대해서 보호 기능을 제공할 수 있다고 한다.
{
key: 'X-XSS-Protection',
value: '1; mode=block'
}
iframe내에서 사이트를 표시할 수 있는지 여부를 나타낸다. 클릭 재킹 공격을 방지할 수 있다고 한다. 최신 브라우저에서 더 나은 지원을 제공하는 CSP의 frame-ancestors
옵션으로 대체됐다고 한다.
브라우저에서 사용할 수 있는 기능과 API를 제어할 수 있다. https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md 여기서 permission option들을 볼 수 있다.
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()'
}
Content-Type
헤더가 설정 안된 콘텐츠의 경우 브라우저가 추측하는것을 방지한다. 이렇게 하면 사용자가 파일을 업로드하고 공유할 수 있는 웹 사이트에 대한 XSS 공격을 방지할 수 있다. 예를 들어 사용자가 이미지를 다운로드하려는데 실행파일(악성파일일 수도 있음)같은 다른 Content-Type
으로 취급되는 경우. 브라우저 확장에도 적용된다함 이 헤더에 유효값은 nosniff
밖에 없다.
{
key: 'X-Content-Type-Options',
value: 'nosniff'
}
현재 웹사이트(오리진)에서 다른 사이트로 이동할 때 얼마나 많은 정보를 브라우저가 포함할 수 있는지 제어한다. https://scotthelme.co.uk/a-new-security-header-referrer-policy/ 여기서 option들을 볼 수 있다.
XSS, 클릭 재킹 및 기타 코드 주입 공격을 방지하는 데 도움이 된다. CSP(Content Security Policy)는 스크립트, 스타일시트, 이미지, 글꼴, 개체, 미디어(오디오, 비디오), iframe 등을 포함한 콘텐츠에 대해 허용된 오리진을 지정할 수 있다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 여기서 옵션을 살펴볼 수 있다.
// 템플릿 리터럴로 이렇게 만들어놓고
// self같은건 ''이걸로 감싸야함
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self';
child-src example.com;
style-src 'self' example.com;
font-src 'self';
`
{
key: 'Content-Security-Policy',
// 요런식으로 주입하자 개행을 공백으로 바꾸어서 붙여줌
value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
}
이놈이