해당 프로젝트는 Next.js 튜토리얼을 따라만든 것입니다. 튜토리얼의 예시코드는 여기에서 확인하실 수 있습니다.
튜토리얼을 따라 만들면 위와 같은 결과물을 얻을 수 있다. 헤더에는 작성자의 프로필이 나타나고 아래에 블로그 포스팅 리스트가 나열된다. 포스팅을 클릭하여 포스팅 내용을 보는 것도 가능하다.
├── components
│ ├── date.js
│ ├── layout.js
│ └── layout.module.css
├── lib
│ └── posts.js
├── pages
│ ├── api
│ │ └── hello.js
│ └── posts
│ │ └── [id].js
│ ├── _app.js
│ └── index.js
├── posts
│ ├── pre-rendering.md
│ └── ssg-ssr.md
├── public
│ └── images
│ └── profile.jpg
└── styles
├── global.css
└── utils.module.css
위의 파일들은 해당 프로젝트에서 편집하는 파일들이다. 각 파일들의 쓰임은 다음과 같다.
components
: 웹 페이지에 사용되는 컴포넌트 관련 파일 저장lib/posts.js
: 블로그 포스팅을 불러오는 SSG 메서드 저장pages
api
: api 저장posts
: 특정 블로그 포스트를 클릭하면 실행되는 url로 동적 경로를 지원posts
: 블로그 포스팅 내용 저장public
: 이미지 같은 정적 자산 저장styles
: css 파일 저장_app.js
와 global.css
모든 페이지에 공통으로 같은 CSS 파일을 적용하고 싶다면 어떻게 해야할까? 페이지마다 CSS 파일을 가져와서 사용할 수도 있지만 페이지가 많아질수록 이는 매우 귀찮은 작업이다.
_app.js
는 어플리케이션의 모든 페이지를 감싸는 최상위 리액트 컴포넌트다. 여기에 글로벌 css 파일을 추가하면 모든 페이지에 같은 css가 적용된다.
import '../styles/global.css';
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}
layout.js
위 사진은 메인 페이지에서 첫번째 포스트를 클릭하면 출력되는 화면이다. 메인 페이지와 마찬가지로 작성자의 프로필이 상단에 출력된다. 공통적으로 사용되는 부분을 컴포넌트로 분리하고 페이지에서 해당 컴포넌트를 가져오는 방식으로 구현하면 코드 중복을 줄이고 일관성 있게 코드를 작성할 수 있다.
import Head from 'next/head';
import Image from 'next/image';
import styles from './layout.module.css';
import utilStyles from '../styles/utils.module.css';
import Link from 'next/link';
const name = 'Chae Hyungwon';
export const siteTitle = 'Next.js Sample Website';
export default function Layout({ children, home }) {
return (
<div className={styles.container}>
<Head>...</Head>
<header className={styles.header}>
{home ? (
<>
<Image
priority
src="/images/profile.jpg"
className={utilStyles.borderCircle}
height={144}
width={144}
alt="Chae Hyungwon"
/>
<h1 className={utilStyles.heading2Xl}>{name}</h1>
</>
) : (
<>
<Link href="/">
<Image
priority
src="/images/profile.jpg"
className={utilStyles.borderCircle}
height={108}
width={108}
alt="ChaeHyungwon"
/>
</Link>
<h2 className={utilStyles.headingLg}>
<Link href="/" className={utilStyles.colorInherit}>
{name}
</Link>
</h2>
</>
)}
</header>
<main>{children}</main>
{!home && (
<div className={styles.backToHome}>
<Link href="/">← Back to home</Link>
</div>
)}
</div>
);
}
화면에 출력되지 않는 <Head>
의 내용은 생략하고 본문만 가져왔다. layout.js
의 Layout
컴포넌트는 children
과 home
을 전달받는다.
children
: 페이지의 메인 내용home
: 메인 페이지 여부children
은 <main>
태그에 감싸져 출력되는 메인 콘텐츠다. home
은 메인 페이지의 유무를 나타내는데 home
이 전달되는 경우, 메인 페이지로 간주한다.
{home ? (
<>
<Image
priority
src="/images/profile.jpg"
className={utilStyles.borderCircle}
height={144}
width={144}
alt='profile'
/>
<h1 className={utilStyles.heading2Xl}>{name}</h1>
</>
) : (
<>
<Link href="/">
<Image
priority
src="/images/profile.jpg"
className={utilStyles.borderCircle}
height={108}
width={108}
alt='profile'
/>
</Link>
<h2 className={utilStyles.headingLg}>
<Link href="/" className={utilStyles.colorInherit}>{name}</Link>
</h2>
</>
)}
첫번째 삼항 연산자를 해석해보자. 메인 페이지인 경우, 프로필이 144 x 144 크기로 출력된다. 메인 페이지가 아닌 경우, 108 x 108 크기로 출력되며 클릭 시 메인 페이지로 이동한다.
{!home && (
<div className={styles.backToHome}>
<Link href="/">← Back to home</Link>
</div>
)}
두번째 삼항 연산자를 보자. 메인 페이지가 아닌 경우, 메인 페이지로 이동하는 링크가 표시된다.
index.js
: 블로그 메인 화면 만들기앞에서 만든 Layout
을 활용하여 블로그 메인 화면을 만들어보자.
import Head from 'next/head';
import Link from 'next/link';
import Layout, { siteTitle } from '../components/layout';
import Date from '../components/date';
import { getSortedPostsData } from '../lib/posts';
import utilStyles from '../styles/utils.module.css';
export default function Home({ allPostsData }) {
return (
<Layout home>
<Head>
<title>{siteTitle}</title>
</Head>
<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}`}>{title}</Link>
<br />
<small className={utilStyles.lightText}>
<Date dateString={date} />
</small>
</li>
))}
</ul>
</section>
</Layout>
);
}
export async function getStaticProps() {
const allPostsData = getSortedPostsData();
return {
props: {
allPostsData
}
};
}
Home
컴포넌트는 allPostsData
를 전달받는다. allPostsData
는 블로그의 모든 포스팅 정보다. 데이터를 전달받으면 리스트 형식으로 출력한다.
posts/[id].js
: 동적 경로 만들기벨로그의 포스팅은 velog.io/@[벨로그 프로필]/[포스팅 제목]
형태의 url을 갖는다. 벨로그처럼 외부 데이터를 활용하여 동적으로 경로를 생성해보자.
posts/[포스팅 id]
형식의 url을 사용하려면 pages
의 하위 디렉토리에 posts
디렉토리를 만들고 그 밑에 [id].js
파일을 만든다.
import Head from 'next/head';
import Layout from '../../components/layout';
import Date from '../../components/date';
import { getAllPostIds, getPostData } from '../../lib/posts';
import utilStyles from '../../styles/utils.module.css';
export default function Post({ postData }) {
return (
<Layout>
<Head>
<title>{postData.title}</title>
</Head>
<article>
<h1 className={utilStyles.headingX1}>{postData.title}</h1>
<div className={utilStyles.lightText}>
<Date dateString={postData.date} />
</div>
<div dangerouslySetInnerHTML={{__html: postData.contentHtml}} />
</article>
</Layout>
);
}
export async function getStaticPaths() {
const paths = getAllPostIds();
return {
paths,
fallback: false
}
}
export async function getStaticProps({ params }) {
const postData = await getPostData(params.id);
return {
props: {
postData
}
}
}
Home
컴포넌트와 마찬가지로 Layout
컴포넌트를 가져와 사용한다. Post
컴포넌트는 특정 포스팅의 정보인 postData
를 전달받는다.
getStaticPaths
는 사전 렌더링할 경로를 결정한다.getStaticPaths
는paths
를getStaticProps
로 전달한다.
lib/posts.js
: 포스팅 데이터 조회하기앞에서 getStaticProps
와 getStaticPaths
메서드가 나왔는데 이에 대한 설명이 없었다. 여기서 getStatic...
에 대해 알아보자.
getStatic...
은 SSG 방식으로 데이터를 렌더링 한다. SSG는 빌드 시에 데이터를 렌더링하기 때문에 자주 수정되지 않는 데이터를 조회할 때 사용하면 좋다.
lib/pages
는 다양한 모듈을 사용한다. fs
나 path
모듈의 경우 내장되어 있지만 gray-matter
나 remark
는 설치가 필요하다.
npm install gray-matter
npm install remark
- gray-matter : 문자열이나 파일의 front-matter를 파싱하는 모듈이다. 마크다운 파일로 블로그를 만들 경우 거의 필수로 사용된다.
- remark : 마크다운 입력값을 파싱하여 마크다운을 출력한다.
remark-html
은 마크다운을 HTML로 컴파일한다.
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
const postsDirectory = path.join(process.cwd(), 'posts');
export function getSortedPostsData() {
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = fileNames.map(fileName => {
const id = fileName.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const matterResult = matter(fileContents);
return {
id,
...matterResult.data
};
});
return allPostsData.sort((a, b) => { return a.data - b.data; });
}
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory);
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');
const matterResult = matter(fileContents);
const processedContent = await remark().use(html).process(matterResult.content);
const contentHtml = processedContent.toString();
return {
id,
contentHtml,
...matterResult.data
};
}
getSortedPostsData
: (in 메인 페이지) 블로그의 전체 포스팅 조회getAllPostIds
: (in 포스트 페이지) 블로그의 전체 포스팅 아이디값 조회getPostData
: (in 포스트 페이지) id에 해당하는 포스트 데이터 조회export async function getStaticPaths() {
const paths = getAllPostIds();
return {
paths,
fallback: false
}
}
getStaticPaths
는 paths
와 함께 fallback: false
를 반환한다. fallback
은 뭘까?
fallback
은 어떤 기능이 제대로 동작되지 않았을 때 대처하는 기능이나 동작을 의미한다. fallback
은 다음 값을 가질 수 있다.
false
: 유효하지 않은 경로라면 404 page 반환
true
: getStaticProps
의 기능을 다음과 같이 변경
getStaticPaths
로부터 반환된 경로들이 빌드 시에 getStaticProps
에 의해 HTML로 렌더링 된다.getStaticProps
함수를 이용하여 HTML과 JSON 파일을 생성한다.blocking
: true
와 비슷하게 동작하지만, 빌드 시에 생성된 경로가 아니라면 fallback 상태를 보여주지 않고 SSR처럼 동작한다.
date-fns
: 날짜 조작하기date-fns는 날짜 형식을 조작하는 라이브러리다. 내장된 라이브러리가 아니기 때문에 별도로 설치해야 한다.
npm install date-fns
import { parseISO, format } from 'date-fns';
export default function Date({ dateString }) {
const date = parseISO(dateString);
return (
<time dateTime={dateString}>
{format(date, 'LLLL d, yyyy')}
</time>
);
}
API는 Application Programming Interface의 줄임말로, 두 어플리케이션 간 서비스 계약으로 볼 수 있다. 요청과 응답을 사용하여 두 어플리케이션이 서로 통신하는 방법을 정의한다.
pages/api
의 모든 하위 파일은 /api/*
로 매핑되어 api의 엔드포인트로 간주된다. 아주 간단한 api를 만들어보자.
/*
/api/hello
*/
export default function handler(req, res) {
res.status(200).json({text: 'Hello'});
}
Next.js를 배포하는 가장 간단한 방법은 Next.js 제작자가 개발한 Vercel 플랫폼을 이용하는 것이다. Vercel은 정적 및 하이브리드 어플리케이션을 위한 서버리스 플랫폼이다.
Vercel을 사용하기 위해서는 Vercel 계정을 만들어야 한다. Continue with Github를 선택하여 회원 가입을 진행했다.
다음으로 소스코드를 올린 레포지토리를 가져와야 한다. 레포지토리를 성공적으로 가져오면 configure project가 뜬다. 이는 수정하지 않고 기본값을 사용해도 된다. deploy 버튼을 누르면 다음과 같이 성공적으로 배포된 것을 확인할 수 있다. https://next-js-practice-gcpl8h6wc-kimyu0218.vercel.app/