8. Next.js와 TypeScript
Next.js 란
: React의 SSR(server side rendering)을 쉽게 구현할 수 있게 도와 주는 간단한 프레임워크
-> React로 개발할 때 SPA를 이용하며 CSR을 하는데, 이로 인해 검색엔진 최적화 부분에 문제가 생긴다.
# CSR : Client Side Rendering
-> CSR을 하면 첫 페이지에서 빈 html 위에 JS 파일을 해석하여 화면을 구성하기 때문에 포털 검색에 노출되지 않는다.
=> Next.js 에서는 Pre-Rendering 을 통해서 페이지를 미리 렌더링한 뒤 완성된 HTML을 가져오기 때문에 바로 렌더링 된 페이지를 전달할 수 있게 된다.
# 리액트에서도 SSR을 지원하지만 구현하기에 굉장히 복잡하기 때문에 Next.js 을 활용하겠다.
# SSR : Server Side Rendering
Server Side Rendering
: 클라이언트가 아닌, 서버에서 페이지를 준비
-> 원래 React 에서는 CSR을 하기 때문에 서버에 영향을 미치지 않고, 서버에서 클라이언트로 보낸 html 도 비어있다.
-> 이 방식은 서버에서 데이터를 가져올 때 지연 시간이 발생한다. (UX 측면에서 좋지 않을 수 있다.)
=> 검색 엔진에 검색 시 내용을 제대로 가져와 읽을 수 없기에 검색 엔진 최적화에 문제가 된다.
Next.js 에서는 SSR을 이용하므로 바로 렌더링 된 페이지를 전달 할 수 있어서 검색엔진 최적화에 좋은 영향을 준다.
설치 방법
npm create-next-app@latest --typescript ./
# 강의와 구성이 다른 이유 : 버전이 다르기 때문
# @latest 가 최신 버전을 의미한다. 강의 속 버전은 12.1.0 으로 보이므로 @12.1.0 으로 설치해주었다.
Next.js 기본 파일 구조
pages
: 페이지들을 생성하는 폴더
-> index.tsx 가 처음 페이지가 된다.
-> _app.tsx 에는 공통되는 레이아웃을 작성한다.
-> 모든 페이지에 공통으로 들어가는 걸 넣어주려면 여기에 넣으면 된다.
# url 을 통해 특정 페이지에 진입하기 전 통과하는 인터셉터 페이지
public
: 이미지 같은 정적 에셋들을 보관하는 폴더
styles
: 스타일링을 처리해주는 폴더
-> 모듈(module) CSS 는 컴포넌트를 종속적으로 스타일링 하기 위한 것이며, 확장자 앞에 module 을 붙여줘야 한다.
next.config.js
: 웹팩에 관한 설정들을 관리하는 파일
-> Nextjs는 웹팩을 기본 번들러로 사용한다.
Pre-rendering
Next.js 는 모든 페이지를 Pre-render 한다.
-> 모든 페이지를 위한 HTML을 Client 사이드에서 자바스크립트로 처리하기 전, 미리 생성한다는 것이다.
-> 그렇기에 검색 엔진 최적화에 도움이 된다.
예시
JS : https://developer.chrome.com/docs/devtools/javascript/disable/
Disable JavaScript - Chrome Developers
Open the Command Menu and run the Disable JavaScript command.
developer.chrome.com
React : https://create-react-app.examples.vercel.com
React App
create-react-app.examples.vercel.com
Next.js : https://next-learn-starter.vercel.app/
Next.js Sample Website
Hello, I’m Shu. I’m a software engineer and a translator (English/Japanese). You can contact me on Twitter. (This is a sample website - you’ll be building a site like this in our Next.js tutorial.)
next-learn-starter.vercel.app
Data Fetching
Nextjs에서 데이터를 가져오는 방법은 여러가지가 있다.
-> 애플리케이션의 사용 용도에 따라서 다른 방법을 사용해주면 된다.
-> 보통 React에서는 데이터를 가져올 때 useEffect안에서 가져온다.
=> Next.js에서는 다른 방법을 사용해서 가져온다.
getStaticProps
getStaticProps 함수를 async 로 export 하면, getStaticProps 에서 리턴되는 props 를 가지고 페이지를 Pre-render 한다. build time 에 페이지를 렌더링한다.
getStaticProps를 사용해야 할 때
- 사용자의 요청보다 build time 에 필요한 데이터를 먼저 가져올 때
- Headless CMS 에서 데이터를 가져올 때
- 데이터를 공개적으로 캐시할 수 있을 때
- 페이지가 미리 렌더링되어야 하고 매우 빨라야할 때
# getStaticProps는 성능을 위해 CDN에서 캐시할 수 있는 HTML 및 JSON 파일을 생성한다.
getStaticPaths
동적 라우팅이 필요할 때 getStaticPaths 로 경로 리스트를 정의하고, HTML에 build time 에 렌더링된다.
Nextjs는 pre-render에서 정적으로 getStaticPaths에서 호출하는 경로들을 가져온다.
paths
: 어떠한 경로가 pre-render 될지를 결정한다.
params
페이지 이름이 pages/posts/[postId]/[commentId] 라면 , params은 postId와 commentId이다.
만약 페이지 이름이 pages/[...slug] 와 같이 모든 경로를 사용한다면, params는 slug 가 담긴 배열이어야한다.
fallback
false 라면 getStaticPaths 로 리턴되지 않는 것은 모두 404가 뜬다.
true 라면 getStaticPaths 로 리턴되지 않는 것은 404로 뜨지 않고, fallback 페이지가 뜨게 된다.
getServerSideProps
getServerSideProps 함수를 async 로 export 하면, Next.js 는 각 요청마다 리턴되는 데이터를 getServerSideProps 로 pre-render한다.
getServerSideProps를 사용해야 할 때
- 데이터를 가져와야하는 페이지를 미리 렌더해야 할 때
- 데이터가 많이 바뀔 때 -> request 를 보낼 때마다 새롭게 데이터를 가져올 수 있게 하기 위해 getServerSideProps 를 사용해준다.
# 서버가 모든 요청에 대한 결과를 계산하고, 추가 구성없이 CDN 에 의해 결과를 캐시할 수 없기 때문에 첫 번째 바이트까지의 시간은 getStaticProps 보다 느리다.
TypeScript 란?
TypeScript 가 나오게 된 배경
: JavaScript 는 원래 클라이언트측 언어로 도입되었다.
-> Node.js의 개발로 인해 JavaScript 를 클라이언트측 뿐만이 아닌 서 측 기술로도 활용할 수 있게 만들었다.
-> JavaScript 코드가 커지고 복잡해질수록 코드를 유지, 관리하고 재사용하기가 어려워졌다.
-> 더욱이 Type 검사 및 컴파일 시 오류 검사의 기능을 수용하지 못하기 때문에, JavaScript 는 본격적인 서버 측 기술로 엔터프라이즈 수준에서 성공하지 못하게 된다.
=> 이것을 해결하기 위해 TypeScript가 제시되었다.
TypeScript 란?
: JavaScript에 Type 을 부여한 언어
-> 자바스크립트의 확장된 언어라고 볼 수 있다.
-> TypeScript 는 JavaScript 와 달리 브라우저에서 실행 하려면 파일을 한번 변환해주어야 한다.
# 이 변환 과정을 컴파일이라고 부른다.
Type System 이란?
: 개발 환경에서 에러를 잡는 걸 도와준다.
-> type annotations를 사용해서 코드를 분석할 수 있다.
-> 오직 개발 환경에서만 활성화된다.
-> 타입 스크립트와 성능 향상과는 관계가 없다.
TypeScript 사용하는 이유
- JavaScript 코드를 단순화하여 더 쉽게 읽고 디버그할 수 있도록 도와준다.
- 오픈 소스이다.
- JavaScript IDE 등을 위한 생산적인 개발 도구를 제공해준다.
- 일반 JavaScript 의 문제점을 개선할 수 있다.
- ES6(ECMAScript 6) 의 모든 이점과 더 많은 생산성을 제공해준다.
- 코드 유형 검사를 통해 JavaScript 를 작성할 때 개발자가 일반적으로 겪는 버그를 줄이는 데 도움이 된다.
Next.js와 TypeScript 만들 앱 소개
: 간단한 블로그 앱을 만들 것이다.
# nextjs 공식 사이트 Documentation 에서 nextjs 를 배우기 위해 만드는 앱. 블로그 포스트 내용은 md 파일로 작성한다.
메인 페이지 UI 만들기(마크다운 파일 생성)
-> api/index.tsx
import type { NextPage } from "next";
import Head from 'next/head'
import Image from 'next/image'
import homeStyles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
<div>
<Head>
<title> GIDO </title>
</Head>
<section className={homeStyles.headingMd}>
<p>[Introduction]</p>
<p>
(hi! this is a doeun's website)
</p>
</section>
<section className={`${homeStyles.headingMd} ${homeStyles.padding1px}`}>
<h2 className={homeStyles.headingLg}>Blog</h2>
<ul className={homeStyles.list}>
</ul>
</section>
</div>
)
}
export default Home
-> Home.module.css
.headingMd {
font-size : 1.2rem;
line-height: 1.5;
}
.padding1px {
padding-top: 1px;
}
.headingLg {
font-size: 1.5rem;
line-height: 1.4;
margin:1rem 0;
}
.List {
list-style: none;
padding: 0;
margin: 0;
}
md 파일 안에 포스트 생성하기
: 포스트들을 생성하면 상세 페이지에서 포스트 목록을 보여준다.
-> 원래는 서버에서 데이터를 가져와야 하는데, 지금은 서버가 없기에 포스트 정보를 임의로 만들어서 넣어줄 것.
markdown 파일이란?
: 텍스트 기반의 마크업 언어로 쉽게 쓰고 읽을 수 있으며 HTML로 변환이 가능하다.
-> 특수기호와 문자를 이용한 매우 간단한 구조의 문법을 사용하여 웹에서도 보다 빠르게 컨텐츠를 작성하고 보다 직관적으로 인식할 수 있다.
-> 마크다운이 최근 각광 받기 시작한 이유는 깃헙에서 사용하는 README.md 덕분이다.
-> 마크다운을 통해서 설치 방법, 소스코드 설명, 이슈 등을 간단하게 기록하고 가독성을 높일 수 있다는 강점이 부각되면서 점점 여러 곳으로 퍼져가게 되고 있다.
posts 폴더 생성, 파일 생성 후 마크다운 작성하기
-> pre-rendering.md
---
title: "Two Forms of Pre-rendering"
date: "2020-01-01"
---
Next.js has two forms of pre-rendering: **Static Generation** and **Server
- side Rendering**. The difference is in **when** it generates the HTML for a page.
- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
-> ssg-ssr.md
title: "When to Use Static Generation v.s. Server-side Rendering"
date: "2020-01-02"
---
We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
You can use Static Generation for many types of pages, including:
- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation
You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
마크다운 파일을 데이터로 추출하기
markdown file
: post에 나올 글들을 파일로 옮겨줬는데, 이를 데이터로 변환시켜줘야 한다.
post에 사용 할 함수들을 만들 폴더와 파일 생성
-> lib폴더를 만들어서 posts.ts 파일을 생성한다.
gray-matter 설치
npm install --save gray-matter
posts.ts 함수 작성하기
-> posts.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const postsDirectory = path.join(process.cwd(), 'posts')
console.log('process.cwd()', process.cwd());
console.log('postDirectory', postsDirectory);
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, 'utf-8')
const matterResult = matter(fileContents)
return {
id,
...(matterResult.data as { date: string; title: string })
}
})
return allPostsData.sort((a, b) => {
if(a.date < b.date) {
return 1
} else {
return -1
}
})
}
---
title: "My First Post"
date: 2023-05-29
author: "First Post"
---
# Welcome to my blog
This is the content of my first post.
TypeScript Type
: 타입이란, 그 value가 가지고 있는 프로퍼티나 함수를 추론할 수 있는 방법이다.
-> string은 다음과 같이 다양한 properties 와 method 를 가지고 있는 것을 알 수 있다.


Types in Typescript
: TypeScript 는 JavaScript 에서 기본으로 제공하는 기본 제공 유형을 상속한다. TypeScript 유형은 다음과 같이 분류된다.
Primitive types (원시 타입)

Object types

Typescript 추가 제공 타입
Any
-> 애플리케이션을 만들 때, 잘 알지 못하는 타입을 표현해야 할 수도 있다.
-> 이 경우, 타입 검사를 하지 않고 그 값들이 컴파일을 통과해야 한다.
=> 이를 위해, any 타입을 사용할 수 있다.
# 하지만 이 타입을 최대한 쓰지 않는게 좋다. noImplicitAny 라는 옵션을 주면 any 를 썼을 때 오류가 발생할 수 있다.
let something: any = "Hello World!";
something = 23;
something = true;
et arr: any[] = ["John", 212, true];
arr.push("Smith");
console.log(arr);
//Output: [ 'John', 212, true, 'Smith' ]
Union
-> TypeScript 를 사용하면 변수 또는 함수 매개변수에 대해 둘 이상의 데이터 유형을 사용할 수 있다.
=> 이것을 유니온 타입이라고 한다.
let code: (string | number);
code = 123;
code = "ABC";
code = false; // Error
let empId: string | number;
empId = 111;
empId = "E111";
empId = true; // Error
Tuple
-> TypeScript에서는 배열 타입을 보다 특수한 형태로 사용할 수 있는 tuple 타입을 지원한다.
-> tuple 에 명시적으로 지정된 형식에 따라 아이템 순서를 설정해야 되고, 추가되는 아이템 또한 tuple 에 명시된 타입만 사용 가능하다.
var employee: [number, string] = [1, "Steve"];
var person: [number, string, boolean] = [1, "Steve", true];
var user: [number, string, boolean, number, string];
user = [1, "Steve", true, 20, "Admin"];
배열 Tuple
var employee : [number, string][];
employee = [[1, "Steve"], [2, "Bill"], [3, "Jeff"]];
Tuple 에 요소 추가
var employee: [number, string] = [1, "Steve"];
employee.push(2, "Bill");
console.log(employee);
//Output: [1, 'Steve', 2, 'Bill']
employee.push(true) // Error
Enum
: enum은 enumerated type(열거형) 을 의미한다.
-> Enum은 값들의 집합을 명명하고 이를 사용하도록 만든다.
-> 여기서는 PrintMedia라 불리는 집합을 기억하기 어려운 숫자 대신 친숙한 이름으로 사용하기 위해 enum을 활용할 수 있다. 열거된 각 PrintMedia는 별도의 값이 설정되지 않은 경우 기본적으로 0부터 시작한다.
enum PrintMedia {
Newspaper, //0
Newsletter, //1
Magazine, //2
Book //3
}
enum 에 설정된 아이템에 값을 할당할 수도 있다. 값이 할당되지 않은 아이템은 이전 아이템의 값에 +1된 값이 설정된다.
enum PrintMedia {
Newspaper = 1,
Newsletter = 50,
Magazine = 55,
Book // 55 + 1
}
숫자 값을 통해 enum 값의 멤버 이름을 도출 할 수 있다.
let type: string = PrintMedia[55] // 'Magazine'
또한 어떠한 언어 코드를 정의하는 코드를 작성할 때 언어의 집합을 만들 때도 enum을 사용 할 수 있다.
이렇게 enum을 이용해서 언어 집합을 만들어주면 어떠한 코드가 어떠한 나라의 언어 코드가 무엇인지 알지 못해도 쉽게 코드를 작성해 줄 수 있고 코드를 읽는 사람 입장에서도 가독성이 높아지게 된다.
export enum LanguageCode { korean = 'ko',
english = 'en',
japanese = 'ja',
chinese = 'zh',
spanish = 'es',
}
const code: LanguageCode = LanguageCode.english
enum과 객체의 차이점
: object는 코드내에서 새로운 속성을 자유롭게 추가할 수 있지만, enum은 선언할 때 이후에 변경할 수 없다.
=> object 의 속성값은 JS가 허용하는 모든 타입이 올 수 있지만, enum 의 속성값으로는 문자열 혹은 숫자만 허용된다.
Void
-> Java와 같은 언어와 유사하게 데이터가 없는 경우 void가 사용된다.
# 예를 들어 함수가 값을 반환하지 않으면 반환 유형으로 void를 지정할 수 있다.
-> 타입이 없는 상태이며, any 와 반대의 의미를 가진다.
-> void 소문자로 사용해야 하며, 주로 함수의 리턴이 없을 때 사용하면 된다.
function sayHi(): void {
console.log('Hi!')
}
let speech: void = sayHi();
console.log(speech); //Output: undefined
Never
-> TypeScript는 절대 발생하지 않을 값을 나타내는 새 Type never를 도입했다.
-> Never 유형은 어떤 일이 절대 일어나지 않을 것이라고 확신할 때 사용된다.
-> 일반적으로 함수의 리턴 타입으로 사용된다.
-> 함수의 리턴 타입으로 never가 사용될 경우, 항상 오류를 리턴하거나 리턴 값을 절대로 내보내지 않음을 의미한다.
=> 이는 무한 루프(loop)에 빠지는 것과 같다.
function throwError(errorMsg: string): never {
throw new Error(errorMsg);
}
function keepProcessing(): never {
while (true) {
console.log('I always do something and never ends.')
}
}
Void 와 Never의 차이
: Void 유형은 값으로 undefind나 null 값을 가질 수 있으며 Never은 어떠한 값도 가질 수 없다.
: TypeScript에서 값을 return하지 않는 함수는 실제로 undefined를 반환한다.
let something: void = null;
let nothing: never = null; // Error: Type 'null' is not assignable to type 'never'
function sayHi(): void {
console.log('Hi!')
}
let speech: void = sayHi();
console.log(speech); // undefined
위의 예에서 볼 수 있듯이 sayHi 함수는 반환 유형이 void인 경우에도 내부적으로 undefined를 반환하기 때문에 speech는 undefined가 된다. Never 유형을 사용하는 경우 void는 Never에 할당할 수 없기 때문에 Speech:never는 컴파일 시간 오류를 발생시킨다.
Type annotation, Type inference
type annotation
: 개발자가 타입을 타입스크립트에게 직접 말해주는 것
type inference
: 타입스크립트가 알아서 타입을 추론하는 것
타입을 추론하지 못해서 타입 annotation을 꼭 해줘야하는 경우
1. any 타입을 리턴하는 경우
coordinates에 hover해보면 const coordinates: any 라고 뜬다. JSON.parse 는 json을 파싱해준다.
인풋으로 들어가는 json을 확인하면 대충 어떤 타입이 리턴될지 개발자는 예상할 수 있지만, TypeScript는 여기까지 지원하지 않는다. (리턴 타입이 일정하지 않으므로 any를 리턴한다고 추론해버린다.)
=> 이 경우에는 타입 애노테이션을 해주어야 한다.
2. 변수 선언을 먼저하고 나중에 초기화하는 경우
변수 선언과 동시에 초기화하면 타입을 추론할 수 있지만, 선언을 먼저하고 나중에 값을 초기화할 때에는 타입을 추론하지 못한다.
3. 변수에 대입될 값이 일정치 못하는 경우
여러 타입이 지정되어야 할 때에는 | 로 여러 타입을 애노테이션 해준다.
Type assertion
type assetion이란?
TypeScript에서는 시스템이 추론 및 분석한 타입 내용을 우리가 원하는 대로 얼마든지 바꿀 수 있다.
-> 이때 "타입 표명(type assertion)"이라 불리는 메커니즘이 사용된다.
# 타입 표명은 프로그래머가 컴파일러에게 내가 너보다 타입에 더 잘 알고 있고, 타입에 대해 의심하지 말라고 하는 것.
=> type assertion 을 사용하면 값의 type을 설정하고 컴파일러에 이를 유추하지 않도록 지시할 수 있다.
as Foo , < Foo >
-> 타입 표명은 위에 두가지 방식으로 표현할 수 있다.
=> 하지만 리액트를 사용할 때는 < Foo > 키워드는 JSX의 문법과 겹치기 때문에 as Foo를 공통적으로 사용하는것을 추천한다.
getStaticProps를 이용한 포스트 리스트 나열
빌드 타임에 포스트 자료 가져오기
export const getStaticProps: GetStaticProps =async () => {
const allPostsData = getSortedPostsData();
return {
props: {
allPostsData
}
}
}
import { getSortedPostsData } from "@/lib/posts";
import type { GetStaticProps, NextPage } from "next";
props으로 포스트 데이터 가져오기
const Home = ({allPostsData}: {
allPostsData: {
date: string
title: string
id: string
}[]
}) => {
리스트 나열하기
<ul className={homeStyles.list}>
{allPostsData.map(({id, title, date}) =>
<li className={homeStyles.listItem} key={id}>
<a>{title}</a>
<br />
<small className={homeStyles.lightText}>
{date}
</small>
</li>
)}
</ul>
포스트 자세히 보기 페이지로 이동(file system 기반의 라우팅)
파일기반 네비게이션 기능
-> React에서는 route를 위해서 react-router라는 라이브러리를 사용하지만 Next.js에는 페이지 개념을 기반으로 구축된 파일 시스템 기반 라우터가 있다.
-> 파일이 페이지 디렉토리에 추가되면 자동으로 경로로 사용할 수 있다.
-> 페이지 디렉토리 내의 파일은 가장 일반적인 패턴을 정의하는 데 사용할 수 있다.
파일 생성 예시
pages/index.js → /
pages/blog/index.js → /blog
pages/blog/first-post.js → /blog/first-post
pages/dashboard/settings/username.js → /dashboard/settings/username
pages/blog/[slug].js → /blog/:slug (/blog/hello-world)
pages/[username]/settings.js → /:username/settings (/foo/settings) pages/post/[...all].js → /post/* (/post/2020/id/title)
포스트 파일 생성
-> pages 내에 posts 폴더를 생성하고, [id]tsx 파일을 생성한다.
Link 함수를 이용한 페이지 이동
blog 내에 post의 title을 클릭하면 자세히보기 페이지로 이동해야 한다.
-> 우선 index.tsx에 Link를 import 한다.
import Link from "next/link";
그리고 import한 Link를 이용해 네비게이션 기능을 넣어준다.
<ul className={homeStyles.list}>
{allPostsData.map(({id, title, date}) =>
<li className={homeStyles.listItem} key={id}>
<Link href= {`/posts/${id}`}>
<a>{title}</a>
</Link>
<br />
<small className={homeStyles.lightText}>
{date}
</small>
</li>
)}
</ul>
포스트 데이터를 가져와서 보여주기(remark)
getStaticPaths
-> 동적 라우팅이 필요할 때 getStaticPaths로 경로 리스트를 정의하고, HTML에 build 시간에 렌더된다.
-> Nextjs는 pre-render에서 정적으로 getStaticPaths 에서 호출하는 경로들을 가져온다.
Post 데이터를 가져와야 하는 경로 목록을 가져오기
-> [id].tsx
export const getStaticPaths: GetStaticPaths = async () => {
const postIds = getAllPostIds();
const paths = postIds.map((post) => ({
params: { id: post.id },
}));
return {
paths,
fallback : false
}
}
-> posts.ts
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory);
return fileNames.map(fileName => {
return {
id: fileName.replace(/\.md$/, '')
}
})
}
전달받은 아이디를 이용해 해당 포스트의 데이터 가져오기
-> [id].tsx
export const getStaticProps: GetStaticProps = async (context) => {
if (!context || !context.params) {
return {
notFound: true,
}
}
}
-> posts.ts
export async function getPostData(id: string) {
const fullPath = path.join(postsDirectory, `${id}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf-8')
const matterResult = matter(fileContents);
const processedContent = await remark().use(remarkHtml).process(matterResult.content);
const contentHtml = processedContent.toString();
return {
id,
contentHtml,
...(matterResult.data as {date: string; title: string; })
}
}
가져온 데이터 화면에서 보여주기
-> [id].tsx
const Post = ( { postData }: {
postData: {
title: string
date: string
contentHtml: string
}
}) => {
return (
<div>
<Head>
<title>{postData.title}</title>
</Head>
<article>
<h1>{postData.title}</h1>
</article>
<div>
{postData.date}
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }}/>
</div>
)
}
블로그앱 스타일링
-> Home.module.css
.container {
max-width: 36rem;
padding: 0 1rem;
margin: 3rem auto 6rem;
}
-> index.tsx
import type { NextPage } from "next";
import Head from 'next/head'
import Image from 'next/image'
import homeStyles from '../styles/Home.module.css'
const Home: NextPage = () => {
return (
<div className={homeStyles.container>
<Head>
<title> GIDO </title>
</Head>
<section className={homeStyles.headingMd}>
<p>[Introduction]</p>
<p>
(hi! this is a doeun's website)
</p>
</section>
<section className={`${homeStyles.headingMd} ${homeStyles.padding1px}`}>
<h2 className={homeStyles.headingLg}>Blog</h2>
<ul className={homeStyles.list}>
</ul>
</section>
</div>
)
}
export default Home
styles 폴더 안에 Post.module.css 파일을 생성.
-> Post.module.css
.container {
max-width: 36rem;
padding: 0 1rem;
margin: 3rem auto 6rem;
}
-> [id].tsx
import postStyle from '../../styles/Post.module.css'
Next.js 13
이번에는 Next.js 13버전을 활용하여 간단한 게시글 목록을 만들어본다.
next app 설치
- next app 설치를 위한 폴더를 만든다.
- 터미널에 아래 명령어를 입력하여 설치.
npx create-next-app@latest --typescript ./
백엔드 서비스를 위한 포켓베이스 이용하기
-> 원래 각각의 Post들은 데이터베이스에서 일일히 하나씩 가져왔었다.
=> 하지만 일일이 하나씩 가져오기에는 시간이 너무 오래걸리니까 pocketbase를 이용할 것이다.
PocketBase - Open Source backend in 1 file
Open Source backend in 1 file with realtime database, authentication, file storage and admin dashboard
pocketbase.io
-> 운영체제에 맞는 것을 설치한 뒤, 폴더로 옮긴다.
포켓베이스 실행
./pocketbase server
-> 서버가 실행되면, 회원가입을 진행하고 로그인 후 New Collection 탭에 들어간다.
-> Name 은 posts 로 해준다.
-> API Rules 는 모두 Unlock 으로 변경 후 Create 버튼을 눌러 생성 완료.

기본 파일 및 컴포넌트 생성
-> page.tsx
const HomePage = () => {
return (
<div>HomePage</div>
)
}
export default HomePage
layout
-> layout.tsx
import Link from "next/link"
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<nav>
<Link href="/">
Home
</Link>
<Link href="/posts">
Post
</Link>
</nav>
<main>
{children}
</main>
</body>
</html>
)
}
Posts 폴더 안에 page 만들기
-> posts/page.tsx
const PostsPage = () => {
return (
<div>PostsPage</div>
)
}
export default PostsPage
Server Component
: next.js는 app 디렉토리 안에 있는 컴포넌트들을 Server Component로 취급한다.
-> 서버에서만 실행되며, 서버에서 렌더링된 후 클라이언트에 결과를 전송한다.
getPosts
-> posts/pages.tsx
import Link from "next/link";
async function getPost() {
const res = await fetch('http://127.0.0.1:8090/api/collections/posts/records');
const data = await res.json();
return data?.items as any[];
}
posts 나열하기
-> posts/pages.tsx
const PostsPage = async () => {
const posts = await getPost();
return (
<div>
<h1>Posts</h1>
{
posts?.map((post) => {
return <PostItem key={post.id} post={post}/>
})}
</div>
)
}
PostItem 컴포넌트
-> posts/pages.tsx
const PostItem = ({ post }: any) => {
const { id, title, created } = post || {};
return (
<Link href={`/posts/${id}`}>
<div>
<h3>{title}</h3>
<p>{created}</p>
</div>
</Link>
)
}
export default PostsPage
Post Detail Page
현재는 post 목록을 클릭하면 404 페이지가 뜬다.
posts 폴더 안에 id 폴더를 만든 뒤, page.tsx를 만든다.
-> posts/[id]/page.tsx
const PostDetailPage = () => {
return (
<div>PostDetailPage</div>
)
}
export default PostDetailPage
데이터 가져오기
-> posts/[id]/page.tsx
async function getPost(postId:string) {
const res = await fetch(
`http//127.0.0.1:8090/api/collections/posts/records/${postId}`
)
}
const PostDetailPage = () => {
return (
<div>PostDetailPage</div>
)
}
export default PostDetailPage
Revalidating Data
: 데이터를 일정 시간마다 재검증한다.
-> posts/[id]/page.tsx
const res = await fetch(
`http//127.0.0.1:8090/api/collections/posts/records/${postId}`,
{ next: { revalidate: 10 } }
)
id에 따른 Post 데이터
-> posts/[id]/page.tsx
async function getPost(postId:string) {
const res = await fetch(
`http://127.0.0.1:8090/api/collections/posts/records/${postId}`,
{ next: { revalidate: 10 } }
)
const data = await res.json();
return data;
}
const PostDetailPage = async ({params}: any) => {
const post = await getPost(params.id);
return (
<div>PostDetailPage</div>
)
}
export default PostDetailPage
UI 생성
-> posts/[id]/page.tsx
async function getPost(postId:string) {
const res = await fetch(
`http://127.0.0.1:8090/api/collections/posts/records/${postId}`,
{ next: { revalidate: 10 } }
)
const data = await res.json();
return data;
}
const PostDetailPage = async ({params}: any) => {
const post = await getPost(params.id);
return (
<div>
<h1>posts/{post.id}</h1>
<div>
<h3>{post.title}</h3>
<p>{post.created}</p>
</div>
</div>
)
}
export default PostDetailPage
error
-> 일부러 에러를 발생시켜 보겠다.
-> posts/[id]/page.tsx
async function getPost(postId:string) {
const res = await fetch(
`http://127.0.0.1:8090/api/collections/posts/records/${postId}`,
{ next: { revalidate: 10 } }
)
//이 부분을 추가
if(!res.ok) {
throw new Error('Failed to fetch data');
}
const data = await res.json();
return data;
}
-> 아래 사이트에 있는 error 예제를 붙여넣어준다.
https://nextjs.org/docs/app/building-your-application/routing/error-handling
-> error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
);
}
-> 콘솔 창에서 에러를 확인해 볼 수 있다.
데이터 생성 컴포넌트 생성
: 새로운 posts 를 생성하는 기능을 Client Component를 이용해 구현해본다.
-> posts/CreatePost.tsx
'use client';
const CreatePost = () => {
return (
<div>CreatePost</div>
)
}
export default CreatePost
UI 생성
-> CreatePost.tsx
const CreatePost = () => {
const [title, setTitle] = useState("")
return (
<form onSubmit={handleSubmit}> {/* 폼 제출 이벤트가 발생하면 handleSubmit 함수를 호출합니다. */}
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit"> {/* 폼 제출 버튼을 생성합니다. */}
Create Post
</button>
</form>
)
}
POST 요청 보내기
-> CreatePost.tsx
const CreatePost = () => {
const [title, setTitle] = useState("")
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await fetch('http://127.0.0.1:8090/api/collections/posts/records', {
method:'POST',
headers: { 'Content-Type': 'application/json'},
body:JSON. stringify({
title
})
})
setTitle('');
}
return (
refresh()
: 현재 경로에서 서버에서 업데이트된 posts를 새로고침하고 가져온다.
-> CreatePost.tsx
'use client';
import React, {useState} from "react";
import { useRouter } from "next/navigation";
const CreatePost = () => {
const [title, setTitle] = useState("")
const router = useRouter();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
await fetch('http://127.0.0.1:8090/api/collections/posts/records', {
method:'POST',
headers: { 'Content-Type': 'application/json'},
body:JSON. stringify({
title
})
})
setTitle('');
router.refresh();
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button type="submit">
Create Post
</button>
</form>
)
}
export default CreatePost