라이브러리를 표방한 React의 장점은 살리고
다양한 편의 기능들을 추가하여 개발자의 필요를 충족시킨 Framework가 바로 Next.js이다.
Next하면 빠질 수 없는 이야기 SSR VS CSR
검색 엔진이 Javascript를 실행하지 않고 크롤링이 가능하게 되고, SEO(검색 엔진 최적화) 된다.
클라이언트로 전달된 HTML 파일 내용 일부를 미리 그려서(Generate) 내려주는 것으로서 클라이언트 렌더링 속도를 빠르게 하여 사용자 체감 속도를 증진시킨다.
Next가 제공하는 기능
- 각종 Optimization
- Hot Code Reloading
- Automatic Routing
- Automatic Code Splitting
- Prefetching
- Dynamic Component
- ...etc
자바스크립트로 화면 자체를 그린다.
첫 화면 랜딩이 느리다 : 용량이 큰 Javascript파일을 받고 랜더링이 되기에 delay 된다.
React와 같은 라이브러리가 CSR이다.
StaticGeneration (SSG)
getStaticProps build time에 Data를 캐싱한다.
데이터가 바뀌지 않는 페이지에는 SSG를 사용한다.
- Marketing page
- Blog post
- E-commerce product listings
- Help and documentation
ServerSideRender(SSR)
getServerSideProps 새로고침 할 때마다 Data를 요청한다.
자주 바뀌어야 하는 데이터를 사용해야 하는 경우 SSR 사용한다.
export async function getServerSideProps() {
const res = await fetch('https://api.spacexdata.com/v3/launches');
const data = await res.json();
return {
props: {data}, // will be passed to the page component as props
};
};
Request time에 동작한다.
SSR은 SSG보다 TTFB(Time to first byte)는 getStaticProps보다 느리다.
프리 랜더링이 필요없다면, SSR을 적용한다.
CSR을 고려한다면 SWR를 추천한다.(CSR은 Request time 이후 동작한다)
export async function getStaticProps() {
const res = await fetch('https://api.spacexdata.com/v3/launches');
const data = await res.json();
return {
props: {data}, // will be passed to the page component as props
};
};
fetch() 함수는 next.js 자체에서 서버와 클라이언트 모두 polyfills 해두었다.
외부 api 접근/ 직접 DB 접근도 가능하다.
getStaticProps는 서버에서만 호출된다.
자바스크립트 번들에 포함되지도 않는다 이미 빌드되어 결과물로 서버로 저장되기 때문이다.
fallback key returned by getStaticPaths
다만 getStaticProps는 pages 또는 일반 파일에서만 사용할 수 있기에 일반 컴포넌트에서는 사용할 수 없다.
그 이유는 페이지가 렌더링되기 전에 React에 필요한 모든 데이터가 있어야 하기 때문이다.
hydration: 페이지가 브라우저에 로드되고 자바스크립트 코드가 실행되면서
페이지가 인터렉티브하게 동작할 상태가 되는 과정.
(js 실행으로 interactive 할 준비 과정)
Next.js는 모든 페이지를 프리랜더링 한다.
Next.js가 미리 HTML을 generate 한다면 더 나은 성능과 검색엔진최적화를 가능하게 만들어 준다.
중요한 점은, Next.js는 프리랜더링을 페이지마다 SSG 방식이나 SSR방식 중 알맞는 데이터 패칭을 선택할 수 있게 한다는 점이다.
모든 페이지가 외부 Data fetching을 필요로 하는 것은 아니기에 SSR 방식이
필요하지 않은 페이지는 SSG를 사용하여 build time에 statically generated되게 만들어 준다.
import Head from 'next/head'
<>
<Head>
<title>First Post</title>
</Head>
<h1>First Post</h1>
<h2>
<Link href="/">
<a>Back to home</a>
</Link>
</h2>
</>
컴포넌트단(JSX)에서 HTML HEAD 부분을 작성한다.
페이지마다 HEAD에 대한 모든 정보를 바꾸어 적용할 수 있다.
import Link from 'next/link';
<Link href="/posts/first-blog">
<a>this page!</a>
</Link>
Link tag로 감싸진 a tag는 필요한 파일만 더 불러오며 이동한다
일반 a tag는 새로 파일을 다시 받아오며 이동한다. pull refresh
스타일을 주고 싶을때에는 Link 태그 내에 a tag에 적용한다.
장점
1.코드 스플리팅
많은 코드들을 유의미하게 적절히 분리되어 있고
새로운 페이지에 접속할 때 필요한 만큼만 로드한다.
수백페이지의 서비스더라도 청크가 크지 않다.
사용자의 새 페이지 진입때 화면을 그리기 위한 리소스 정리가 빨라 사용자 경험 긍정적.
2.프리패칭
화면에 링크 태그로 연결된 페이지 리소스를(page 내에 있는) 미리 받아온다.
외부 사이트로 이동할때에는 Link tag를 사용하지 않는다.
import Image from 'next/image'
<Image
src="/images/profile2.jpg" // Route of the image file
height={144} // Desired size with correct aspect ratio
width={144} // Desired size with correct aspect ratio
alt="Your Name"
/>
HTML의 img tag가 아닌 Next.js에서 제공하는 Image tag를 사용하면 이미지에 맞게 Resizing, Optimizing 기능을 제공한다.
빌드타임에 영향을 주지 않는다.
jsx 내에서 CSS,SCSS스타일링이 가능하다.
스타일드 컴포넌트, 이모션, 테일윈드도 사용 가능하다.
github.com/vercel/next.js/tree/canary/examples/
최상위 루트에 components 폴더 생성 후 layout.js,layout.module.css 생성
// layout.js
import styles from './layout.module.css'
export default function Layout({ children }) {
return <div className={styles.container}>{children}</div>
}
//layout.module.css
.container {
max-width: 36rem;
padding: 0 1rem;
margin: 3rem auto 6rem;
}
// layout.js 를 적용할 posts에 import
import Layout from '../../components/layout'
export default function FirstPost() {
return (
<Layout>
<Head>
<title>First Post</title>
</Head>
<h1>First Post</h1>
<h2>
<Link href="/">
<a>Back to home</a>
</Link>
</h2>
</Layout>
)
}
1. 최상위 루트에 styles 폴더 생성 후 global.css 생성
2. pages 폴더에 _app.js 생성
//_app.js
import "../styles/global.css"
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />
}
//styles/global.css
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
line-height: 1.6;
font-size: 18px;
}
* {
box-sizing: border-box;
}
a {
color: #0070f3;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
img {
max-width: 100%;
display: block;
}
_app.js에는 css뿐만 아니라 모든 페이지의 공통적인 동작이 필요할 때에도 작성할 수 있다.
배열 생성 함수를 lib/posts.js
에 적용한다.
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory)
return fileNames.map(fileName => {
return {
params: {
id: fileName.replace(/\.md$/, '')
}
}
})
}
import { getAllPostIds } from '../../lib/posts'
export async function getStaticPaths() {
const paths = getAllPostIds()
// 다음과같은 object array를 리턴하게 만들어 준다.
// [
// {
// params: {
// id: 'ssg-ssr'
// }
// },
// {
// params: {
// id: 'pre-rendering'
// }
// }
// ]
return {
paths, // [{params: { id: 'ssg-ssr' }, ... ] 와 같은 형태
fallback: false
}
}
배열 하나당 하나의 페이지가 만들어진다.
fallback: false
: getStaticPaths에서 주지 않은 id를 URI에 입력한다면 404 error가 출력되게 처리한다.
3. posts.js에 getPostData 작성
import fs from 'fs'
//프로젝트 내부의 경로
import path from 'path'
//라이브러리 설치
import matter from 'gray-matter'
import { remark } from 'remark'
import html from 'remark-html'
// process.cwd() 루트 + posts : md 파일 저장소
const postsDirectory = path.join(process.cwd(), 'posts')
export function getSortedPostsData() {
// Get file names under /posts
const fileNames = fs.readdirSync(postsDirectory)
const allPostsData = fileNames.map(fileName => {
// Remove ".md" from file name to get id
const id = fileName.replace(/\.md$/, '')
// Read markdown file as string
const fullPath = path.join(postsDirectory, fileName)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents)
// Combine the data with the id
return {
id,
...matterResult.data
}
})
// Sort posts by date
return allPostsData.sort(({ date: a }, { date: b }) => {
if (a < b) {
return 1
} else if (a > b) {
return -1
} else {
return 0
}
})
}
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory)
// Returns an array that looks like this:
// [
// {
// params: {
// id: 'ssg-ssr'
// }
// },
// {
// params: {
// id: 'pre-rendering'
// }
// }
// ]
return fileNames.map(fileName => {
return {
params: {
id: fileName.replace(/\.md$/, '')
}
}
})
}
export async function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents)
// Use remark to convert markdown into HTML string
const processedContent = await remark()
.use(html)
.process(matterResult.content)
const contentHtml = processedContent.toString()
// Combine the data with the id and contentHtml
return {
id,
contentHtml,
...matterResult.data
}
}
4.[id].js 수정 작성
import Layout from '../../components/layout'
import { getAllPostIds, getPostData } from '../../lib/posts'
import Head from 'next/head'
import Date from '../../components/date'
import utilStyles from '../../styles/utils.module.css'
export async function getStaticProps({ params }) {
const postData = await getPostData(params.id)
return {
props: {
postData
}
}
}
export async function getStaticPaths() {
const paths = getAllPostIds();
// Returns an array that looks like this:
// [
// {
// params: {
// id: 'ssg-ssr'
// }
// },
// {
// params: {
// id: 'pre-rendering'
// }
// }
// ]
return {
paths,
fallback: false
}
}
export default function Post({ postData }) {
return (
<Layout>
<Head>
<title>{postData.id}</title>
</Head>
<article>
<h1 className={utilStyles.headingXl}>{postData.title}</h1>
<div className={utilStyles.lightText}>
<Date dateString={postData.date} />
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</article>
</Layout>
)
}
import Date from '../components/date'
<main>
<h1 className="title">
Eremes Kim<br/>
<span>this is</span><Link href="/posts/first-post">
<a > first-blog </a>
</Link>
<span>using next.js!</span>
</h1>
<Image
src="/images/profile2.jpg" // Route of the image file
height={122} // Desired size with correct aspect ratio
width={122} // Desired size with correct aspect ratio
alt="Your Name"
/>
<p className="description">
exciting <code>next.js</code> :)
</p>
<Link href="/posts/second-post"><a style={{color:"#914f14",fontWeight:"bold"}}>Introduce MYSELF</a></Link>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<h2 className={utilStyles.headingLg}>Blog</h2>
<ul className={utilStyles.list}>
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
<Link href={`/posts/${id}`}>
<a>{title}</a>
</Link>
<br />
<small className={utilStyles.lightText}>
<Date dateString={date} />
</small>
</li>
))}
</ul>
</section>
</main>
);
}