Typescript 란?
JavaScript는 원래 클라이언트 측 언어로 도입되었다. 그런데 Node.js의 개발로 인해 JavaScript를 클라이언트 측 뿐만이 아닌 서버 측 기술로도 활용되게 만들었다. 그러나 JavaScript 코드가 커질수록 소스코드가 더 복잡해져서 코드를 유지/관리하고 재사용하기가 어려워졌다. 더욱이 Type 검사 및 컴파일 시 오류 검사의 기능을 수용하지 못하기 때문에 JavaScript가 본격적인 서버 측 기술로 엔터프라이즈 수준에서 성공하지 못한다. 이 간극을 메우기 위해 TypeScript가 제시되었다.
TypeScript는 JavaScript에 타입을 부여한 언어이다. 자바스크립트의 확장된 언어라고 볼 수 있다. TypeScript는 JavaScript와 달리 브라우저에서 실행 하려면 파일을 한번 변환해주어야 한다. 이 변환 과정을 컴파일 (complile) 이라고 부른다.
Nextjs와 Typescript 만들 앱 소개
아주 간단한 블로그 앱이다.
nextjs 공식 사이트 Documentation 에서 nextjs 를 배우기 위해 만드는 앱. 블로그 포스트 내용은 md 파일로 작성.
메인 페이지 UI 만들기(마크다운 파일 생성)
<api/index.tsx>
// Next.js에서 페이지를 타입으로 정의하고 임포트합니다.
import type { NextPage } from "next";
// Next.js의 내장 Head 컴포넌트를 임포트합니다. 이를 사용하면 개별 페이지의 <head> 태그를 커스터마이즈 할 수 있습니다.
import Head from 'next/head'
// Next.js의 내장 Image 컴포넌트를 임포트합니다. 이를 사용하면 이미지를 최적화하거나 레이지 로딩(lazy loading)을 할 수 있습니다.
import Image from 'next/image'
// CSS 모듈을 임포트합니다. CSS 모듈은 각 클래스를 고유한 이름으로 컴파일하여, CSS 클래스가 전역 범위로 충돌하는 것을 방지합니다.
import homeStyles from '../styles/Home.module.css'
// Home 컴포넌트를 정의합니다. 이 컴포넌트는 NextPage 타입을 가집니다, 이는 Next.js의 페이지 컴포넌트임을 나타냅니다.
const Home: NextPage = () => {
return (
<div>
// Head 컴포넌트를 사용하여 페이지의 <head> 태그를 정의합니다.
<Head>
// 페이지의 제목을 설정합니다.
<title>Minseong Kim</title>
</Head>
// CSS 모듈의 headingMd 클래스를 사용하는 섹션을 생성합니다.
<section className={homeStyles.headingMd}>
// Minseong Kim 소개문을 추가합니다.
<p>[Minseong Kim Introduction]</p>
<p>
// 웹사이트임을 설명하는 문구를 추가합니다.
(this is a website)
</p>
</section>
// CSS 모듈의 headingMd와 padding1px 클래스를 모두 사용하는 섹션을 생성합니다.
<section className={`${homeStyles.headingMd} ${homeStyles.padding1px}`}>
// CSS 모듈의 headingLg 클래스를 사용하는 h2 헤딩을 생성합니다.
<h2 className={homeStyles.headingLg}>Blog</h2>
// CSS 모듈의 list 클래스를 사용하는 빈 목록을 생성합니다.
<ul className={homeStyles.list}>
</ul>
</section>
</div>
)
}
// Home 컴포넌트를 기본 내보내기로 설정합니다. 이를 통해 다른 파일에서 이 컴포넌트를 임포트할 수 있습니다.
export default Home
<Home.module.css>
/* headingMd 클래스는 중간 크기의 제목에 사용되며, 폰트 크기와 줄 간격을 설정합니다. */
.headingMd {
font-size : 1.2rem; /* 폰트 크기를 1.2rem으로 설정합니다. rem은 상위 요소의 폰트 크기에 상대적인 단위입니다. */
line-height: 1.5; /* 줄 간격을 1.5로 설정합니다. */
}
/* padding1px 클래스는 상단 패딩을 1px로 설정합니다. */
.padding1px {
padding-top: 1px; /* 상단 패딩을 1px로 설정합니다. */
}
/* headingLg 클래스는 큰 크기의 제목에 사용되며, 폰트 크기, 줄 간격, 그리고 마진을 설정합니다. */
.headingLg {
font-size: 1.5rem; /* 폰트 크기를 1.5rem으로 설정합니다. */
line-height: 1.4; /* 줄 간격을 1.4로 설정합니다. */
margin:1rem 0; /* 위 아래 마진을 1rem으로 설정하고 좌우 마진은 0으로 설정합니다. */
}
/* List 클래스는 리스트에 사용되며, 리스트 스타일을 없애고 패딩과 마진을 0으로 설정합니다. */
.List {
list-style: none; /* 리스트 스타일을 없앱니다. 이렇게 하면 불릿이나 번호가 표시되지 않습니다. */
padding: 0; /* 패딩을 0으로 설정합니다. */
margin: 0; /* 마진을 0으로 설정합니다. */
}
포스트들을 생성하면 상세페이지에서 보여줄건데, 서버에서 원래는 그러한 데이터를 가져와야 하는데, 지금은 서버가 없기에 포스트 정보를 임의로 만들어서 넣어줄 것이다.
Markdown은 텍스트 기반의 마크업언어로 쉽게 쓰고 읽을 수 있으며 HTML로 변환이 가능하다. 특수기호와 문자를 이용한 매우 간단한 구조의 문법을 사용하여 웹에서도 보다 빠르게 컨텐츠를 작성하고 보다 직관적으로 인식할 수 있다. 마크다운이 최근 각광받기 시작한 이유는 깃헙(https://github.com)에서 사용하는 README.md 덕분이다. 마크다운을 통해서 설치 방법, 소스코드 설명, 이슈 등을 간단하게 기록하고 가독성을 높일 수 있다는 강점이 부각되면서 점점 여러 곳으로 퍼져가게 되고 있다.
<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.
posts 폴더 생성
파일 생성 후 마크다운 작성하기
- **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.
위와 같이 posts 내 md파일에 복사 붙여넣기를 해준다.
마크다운 파일을 데이터로 추출하기
post에 나올 글들을 파일로 옮겨줬는데, 이를 데이터로 변환시켜줘야 한다.
lib폴더를 만들어서 posts.ts 파일을 생성한다.
터미널 창에 입력해서 설치하자.
npm install --save gray-matter
<posts.ts>
// 파일시스템 모듈을 가져옵니다. 이를 통해 Node.js에서 파일시스템과 상호 작용할 수 있습니다.
import fs from 'fs'
// 경로 관련 유틸리티를 제공하는 모듈을 가져옵니다.
import path from 'path'
// Markdown 파일의 frontmatter를 파싱하는 라이브러리를 가져옵니다.
import matter from 'gray-matter'
// 현재 작업 디렉토리 (즉, 프로젝트 루트)에 있는 'posts' 디렉토리를 가리키는 경로를 생성합니다.
const postsDirectory = path.join(process.cwd(), 'posts')
// 현재 작업 디렉토리를 콘솔에 출력합니다.
console.log('process.cwd()', process.cwd());
// 'posts' 디렉토리 경로를 콘솔에 출력합니다.
console.log('postDirectory', postsDirectory);
// 모든 post 데이터를 가져오고, 날짜 순으로 정렬하는 함수를 정의합니다.
export function getSortedPostsData() {
// 'posts' 디렉토리에 있는 모든 파일의 이름을 가져옵니다.
const fileNames = fs.readdirSync(postsDirectory)
// 각 파일을 파싱하여 필요한 데이터를 가져옵니다.
const allPostsData = fileNames.map(fileName => {
// 파일의 확장자를 제거하여 post의 ID를 생성합니다.
const id = fileName.replace(/\.md$/, '')
// post 파일의 전체 경로를 생성합니다.
const fullPath = path.join(postsDirectory, fileName)
// post 파일의 내용을 읽어옵니다.
const fileContents = fs.readFileSync(fullPath, 'utf-8')
// 파일 내용에서 frontmatter를 파싱합니다.
const matterResult = matter(fileContents)
// 각 post의 데이터를 객체로 반환합니다.
return {
id,
// 각 post의 frontmatter에서 date와 title을 가져옵니다.
...(matterResult.data as { date: string; title: string })
}
})
// 모든 post 데이터를 날짜 순으로 정렬합니다. (Sorting)
return allPostsData.sort((a, b) => {
if(a.date < b.date) {
return 1
} else {
return -1
}
})
}
파싱 - 데이터를 분석하고 이해하는 과정. 데이터의 구조를 이해하거나 특정 정보를 추출할 수 있다.
frontmatter - Markdown 파일의 최상단에 위치하는 메타데이터 영역
---
title: "My First Post"
date: 2022-10-01
author: "John Doe"
---
# Welcome to my blog
This is the content of my first post.
Typescript Type
아래 이미지를 보면, string은 다음과 같이 다양한 properties와 method를 가지고 있는 것을 알 수 있다.
타입이란, 그 value가 가지고 있는 프로퍼티나 함수를 추론할 수 있는 방법이다.
TypeScript는 JavaScript에서 기본으로 제공하는 기본 제공 유형(built-in types)을 상속한다. TypeScript 유형은 다음과 같이 분류된다.
Typescript 추가 제공 타입
애플리케이션을 만들 때, 잘 알지 못하는 타입을 표현해야 할 수도 있다. 이 값들은 사용자로부터 받은 데이터나 서드파티 라이브러리 같은 동적인 컨텐츠에서 올 수도 있다. 이 경우, 타입 검사를 하지 않고, 그 값들이 컴파일 시간에 검사를 통과하길 원한다. 이를 위해, 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' ]
TypeScript를 사용하면 변수 또는 함수 매개변수에 대해 둘 이상의 데이터 유형을 사용할 수 있다. 이것을 유니온 타입이라고 한다.
let code: (string | number);
code = 123; // OK
code = "ABC"; // OK
code = false; // Compiler Error
let empId: string | number;
empId = 111; // OK
empId = "E111"; // OK
empId = true; // Compiler Error
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];// declare tuple variable
user = [1, "Steve", true, 20, "Admin"];// initialize tuple variable
var employee : [number, string][];
employee = [[1, "Steve"], [2, "Bill"], [3, "Jeff"]];
var employee: [number, string] = [1, "Steve"];
employee.push(2, "Bill");
console.log(employee); //Output: [1, 'Steve', 2, 'Bill']
employee.push(true)
튜플에서 'number | string'은 숫자와 문자열 값만 저장할 수 있다.
enum은 enumerated type(열거형)을 의미한다.
Enum은 값들의 집합을 명명하고 이를 사용하도록 만든다.
여기서는 PrintMedia라 불리는 집합을 기억하기 어려운 숫자 대신 친숙한 이름으로 사용하기 위해 enum을 활용할 수 있다. 열거된 각 PrintMedia는 별도의 값이 설정되지 않은 경우 기본적으로 0부터 시작한다.
enum PrintMedia {
Newspaper, //0
Newsletter, //1
Magazine, //2
Book //3
}
아래 코드에서 mediaType 변수에 할당된 값은 3이다. 설정된 PrintMedia 열거형 데이터의 Book 의 값이 숫자 3이기 때문이다.
let mediaType: number = PrintMedia.Book // 3
enum 에 설정된 아이템에 값을 할당할 수도 있다. 값이 할당되지 않은 아이템은 이전 아이템의 값에 +1된 값이 설정된다.
enum PrintMedia {
Newspaper = 1,
Newsletter = 50,
Magazine = 55,
Book // 55 + 1
}
아래 코드에서 mediaType 변수에 할당된 값은 56이다. 설정된 PrintMedia 열거형 데이터의 Book의 값이 숫자 56이기 때문이다.
let mediaType: number = PrintMedia.Book // 56
enum 타입의 편리한 기능으로 숫자 값을 통해 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과 JS의 object를 사용하는 것과 별 차이가 없어 보인다. 사실 enum은 그 자체로 객체이기도 하다.
그래서 Object.keys(LanguageCode) 를 하면 실제 키 값이 배열에 담겨 나온다. => ['korean', 'english']
Object.values(LanguageCode)를 하면 value 값이 ...=> ['ko', 'en']
Java와 같은 언어와 유사하게 데이터가 없는 경우 void가 사용된다. 예를 들어 함수가 값을 반환하지 않으면 반환 유형으로 void를 지정할 수 있다.
타입이 없는 상태이며, any 와 반대의 의미를 가진다.
void 소문자로 사용해야 하며, 주로 함수의 리턴이 없을 때 사용하면 된다.
function sayHi(): void {
console.log('Hi!')
}
let speech: void = sayHi();
console.log(speech); //Output: undefined
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 유형은 값으로 undefind나 null 값을 가질 수 있으며 Never은 어떠한 값도 가질 수 없다.
let something: void = null;
let nothing: never = null; // Error: Type 'null' is not assignable to type 'never'
TypeScript에서 값을 return하지 않는 함수는 실제로 undefined를 반환한다.
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
개발자가 타입을 타입스크립트에게 직접 말해주는 것
타입스크립트가 알아서 타입을 추론하는 것
coordinates에 hover해보면 const coordinates: any 라고 뜨는 것을 볼 수 있다. JSON.parse 는 json을 파싱해준다. 인풋으로 들어가는 json을 확인하면 대충 어떤 타입이 리턴될지 개발자는 예상할 수 있지만, TypeScript는 여기까지 지원하지 않는다. 리턴 타입이 일정하지 않으므로 any를 리턴한다고 추론해버린다. 그러므로 이 경우에는 타입 애노테이션을 해주어야 한다.
변수 선언과 동시에 초기화하면 타입을 추론할 수 있지만, 선언을 먼저하고 나중에 값을 초기화할 때에는 타입을 추론하지 못한다.
여러 타입이 지정되어야 할 때에는 | (or statement) 로 여러 타입을 애노테이션 해준다.
type assertion
TypeScript에서는 시스템이 추론 및 분석한 타입 내용을 우리가 원하는 대로 얼마든지 바꿀 수 있다. 이때 "타입 표명(type assertion)"이라 불리는 메커니즘이 사용된다. TypeScript의 타입 표명은 프로그래머가 컴파일러에게 내가 너보다 타입에 더 잘 알고 있고, 나의 주장에 대해 의심하지 말라고 하는 것과 같다.
type assertion을 사용하면 값의 type을 설정하고 컴파일러에 이를 유추하지 않도록 지시할 수 있다. 이것은 프로그래머로서 TypeScript가 자체적으로 추론할 수 있는 것보다 변수 유형에 대해 더 잘 이해하고 있을 때이다.
컴파일러는 foo type이 속성이 없는 {}라고 가정하기 때문에 위의 예에서는 컴파일러 오류가 발생한다. 그러나 아래와 같이 type assertion을 사용하면 이러한 상황을 피할 수 있다.
타입 표명은 위에 두가지 방식으로 표현할 수 있다. 하지만 리액트를 사용할 때는 < Foo > 키워드는 JSX의 문법과 겹치기 때문에 as Foo를 공통적으로 사용하는것을 추천한다.
getStaticProps를 이용한 포스트 리스트 나열
export const getStaticProps: GetStaticProps =async () => {
const allPostsData = getSortedPostsData();
//getSortedPostsData() 함수는 posts.ts에서 만든 getSortedPostsData() 함수를
//export한 다음에 코드 최상단에 import해서 가져온 것이다.
return {
props: {
allPostsData
}
}
}
import { getSortedPostsData } from "@/lib/posts";
import type { GetStaticProps, NextPage } from "next";
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 기반의 라우팅)
밑줄 그어진 post의 title을 클릭하면 내용을 볼 수 있게끔 네비게이션 기능을 넣을 것이다.
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 파일을 생성하자.
blog 내에 post의 title을 클릭하면 자세히보기 페이지로 이동해야 한다.
우선 index.tsx에 Link를 import 해주자.
import Link from "next/link";
그리고 import한 Link를 이용해 다음과 같이 네비게이션 기능을 넣어주자.
강의에서는 {title}을 a 태그로 감싸고 있지만, 최신 버전의 Next.js에서는 Link 컴포넌트 내부에 a 태그를 사용하지 않아야 한다. 대신에, Link 컴포넌트 자체가 클릭 가능한 요소로 작동한다.
밑줄 친 부분을 보면, 파일 기반의 라우팅이 잘 되는 것을 확인할 수 있다.
포스트 데이터를 가져와서 보여주기(remark)
동적 라우팅이 필요할 때 getStaticPaths로 경로 리스트를 정의하고, HTML에 build 시간에 렌더된다.
Nextjs는 pre-render에서 정적으로 getStaticPaths 에서 호출하는 경로들을 가져온다.
<[id].tsx>
// Next.js의 getStaticPaths 함수를 정의합니다. 이 함수는 빌드 시에 호출되며, 동적 라우팅을 위한 경로 목록을 제공합니다.
export const getStaticPaths: GetStaticPaths = async () => {
// 모든 게시물 ID를 가져옵니다.
const postIds = getAllPostIds();
// 각 ID를 params 객체로 포함하는 경로 객체로 변환합니다.
const paths = postIds.map((post) => ({
params: { id: post.id },
}));
// 경로 목록을 반환합니다.
return {
paths,
fallback : false
}
}
false 라면 getStaticPaths로 리턴되지 않는 것은 모두 404 페이지가 뜬다.
true라면 getStaticPaths로 리턴되지 않는 것은 404로 뜨지 않고 , fallback 페이지가 뜨게 된다.
<posts.ts>
// 모든 게시물 ID를 가져오는 함수를 정의합니다.
export function getAllPostIds() {
// 'posts' 디렉토리에 있는 모든 파일 이름을 가져옵니다.
const fileNames = fs.readdirSync(postsDirectory);
// 파일 이름 목록을 반복하며, 각 파일 이름에서 확장자를 제거한 후 반환합니다.
// 이렇게 해서 모든 게시물 ID의 배열을 만듭니다.
return fileNames.map(fileName => {
return {
id: fileName.replace(/\.md$/, '')
}
})
}
<[id].tsx>
// Next.js의 getStaticProps 함수를 정의합니다. 이 함수는 빌드 시에 각 경로에 대해 호출되며, 페이지에 필요한 데이터를 제공합니다.
export const getStaticProps: GetStaticProps = async (context) => {
// context.params가 정의되어 있는지 확인합니다.
if (!context || !context.params) {
return {
notFound: true, // 이 경우 404 페이지를 반환합니다.
}
}
}
<posts.ts>
// 특정 ID의 게시물 데이터를 가져오는 비동기 함수를 정의합니다.
export async function getPostData(id: string) {
// 게시물 파일의 전체 경로를 생성합니다.
const fullPath = path.join(postsDirectory, `${id}.md`)
// 게시물 파일의 내용을 가져옵니다.
const fileContents = fs.readFileSync(fullPath, 'utf-8')
// 파일 내용에서 front matter를 파싱합니다.
const matterResult = matter(fileContents);
// remark()와 remark-html 플러그인을 사용하여 Markdown 내용을 HTML로 변환합니다.
const processedContent = await remark().use(remarkHtml).process(matterResult.content);
// 변환된 HTML을 문자열로 변환합니다.
const contentHtml = processedContent.toString();
// id, 변환된 HTML, 그리고 front matter 데이터를 반환합니다.
return {
id,
contentHtml,
...(matterResult.data as {date: string; title: string; })
}
}
위 코드가 ssg-ssr.md를 html 형식으로 바꿔준다.
<[id].tsx>
// Post 컴포넌트를 정의합니다. 이 컴포넌트는 각 게시물 페이지의 레이아웃을 정의합니다.
const Post = ( { postData }: {
postData: {
title: string
date: string
contentHtml: string
}
}) => {
// 게시물의 데이터를 받아서 UI를 렌더링합니다.
return (
<div>
<Head>
<title>{postData.title}</title>
</Head>
<article>
<h1>{postData.title}</h1>
</article>
<div>
{postData.date}
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }}/>
</div>
)
}
<[id].tsx>
// Next.js의 정적 생성 함수를 가져옵니다.
import { GetStaticPaths, GetStaticProps } from 'next'
// React를 import 합니다.
import React from 'react'
// 게시물 데이터를 가져오는 함수를 import 합니다.
import { getAllPostIds, getSortedPostsData, getPostData} from '../../lib/posts'
// Next.js의 Head 컴포넌트를 import 합니다. 이 컴포넌트를 사용하면 HTML 문서의 <head>를 조작할 수 있습니다.
import Head from 'next/head'
// Post 컴포넌트를 정의합니다. 이 컴포넌트는 각 게시물 페이지의 레이아웃을 정의합니다.
const Post = ( { postData }: {
postData: {
title: string
date: string
contentHtml: string
}
}) => {
// 게시물의 데이터를 받아서 UI를 렌더링합니다.
return (
<div>
<Head>
<title>{postData.title}</title>
</Head>
<article>
<h1>{postData.title}</h1>
</article>
<div>
{postData.date}
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }}/>
</div>
)
}
// Post 컴포넌트를 default로 export 합니다.
export default Post
// Next.js의 getStaticPaths 함수를 정의합니다. 이 함수는 빌드 시에 호출되며, 동적 라우팅을 위한 경로 목록을 제공합니다.
export const getStaticPaths: GetStaticPaths = async () => {
// 모든 게시물 ID를 가져옵니다.
const postIds = getAllPostIds();
// 각 ID를 params 객체로 포함하는 경로 객체로 변환합니다.
const paths = postIds.map((post) => ({
params: { id: post.id },
}));
// 경로 목록을 반환합니다.
return {
paths,
fallback : false
}
}
// Next.js의 getStaticProps 함수를 정의합니다. 이 함수는 빌드 시에 각 경로에 대해 호출되며, 페이지에 필요한 데이터를 제공합니다.
export const getStaticProps: GetStaticProps = async (context) => {
// context.params가 정의되어 있는지 확인합니다.
if (!context || !context.params) {
return {
notFound: true, // 이 경우 404 페이지를 반환합니다.
}
}
// 선택된 게시물의 데이터를 가져옵니다.
const postData = await getPostData(context.params.id as string);
// 게시물 데이터를 Post 컴포넌트에 prop으로 전달합니다.
return {
props: {
postData,
},
};
};
<posts.ts>
// Node.js의 파일 시스템 모듈을 가져옵니다. 이 모듈을 이용해 서버의 파일 시스템을 조작할 수 있습니다.
import fs from 'fs'
// Node.js의 경로 모듈을 가져옵니다. 이 모듈은 파일 및 디렉토리 경로를 조작하는 데 도움이 됩니다.
import path from 'path'
// gray-matter 라이브러리를 가져옵니다. 이 라이브러리를 이용해 Markdown 파일의 front matter를 파싱할 수 있습니다.
import matter from 'gray-matter'
// remark-html 라이브러리를 가져옵니다. 이 라이브러리는 Markdown을 HTML로 변환하는 데 사용됩니다.
import remarkHtml from 'remark-html';
// remark 라이브러리를 가져옵니다. 이 라이브러리는 Markdown을 파싱하고 조작하는 데 사용됩니다.
import { remark } from 'remark';
// 프로젝트 루트에 있는 'posts' 디렉토리의 경로를 가져옵니다.
const postsDirectory = path.join(process.cwd(), './posts')
// 작업 디렉토리 경로와 'posts' 디렉토리 경로를 콘솔에 출력합니다. 디버깅에 도움이 됩니다.
console.log('process.cwd()', process.cwd());
console.log('postDirectory', postsDirectory);
// 모든 게시물 데이터를 가져오고 날짜를 기준으로 정렬하는 함수를 정의합니다.
export function getSortedPostsData() {
// 'posts' 디렉토리에 있는 모든 파일 이름을 가져옵니다.
const fileNames = fs.readdirSync(postsDirectory)
// 파일 이름 목록을 반복하며 각 파일의 데이터를 가져옵니다.
const allPostsData = fileNames.map(fileName => {
// 파일의 확장자를 제거하여 게시물 ID를 만듭니다.
const id = fileName.replace(/\.md$/, '');
// 게시물 파일의 전체 경로를 생성합니다.
const fullPath = path.join(postsDirectory, fileName)
// 파일의 내용을 가져옵니다.
const fileContents = fs.readFileSync(fullPath, 'utf-8')
// 파일 내용에서 front matter를 파싱합니다.
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
}
})
}
// 모든 게시물 ID를 가져오는 함수를 정의합니다.
export function getAllPostIds() {
// 'posts' 디렉토리에 있는 모든 파일 이름을 가져옵니다.
const fileNames = fs.readdirSync(postsDirectory);
// 파일 이름 목록을 반복하며, 각 파일 이름에서 확장자를 제거한 후 반환합니다.
// 이렇게 해서 모든 게시물 ID의 배열을 만듭니다.
return fileNames.map(fileName => {
return {
id: fileName.replace(/\.md$/, '')
}
})
}
// 특정 ID의 게시물 데이터를 가져오는 비동기 함수를 정의합니다.
export async function getPostData(id: string) {
// 게시물 파일의 전체 경로를 생성합니다.
const fullPath = path.join(postsDirectory, `${id}.md`)
// 게시물 파일의 내용을 가져옵니다.
const fileContents = fs.readFileSync(fullPath, 'utf-8')
// 파일 내용에서 front matter를 파싱합니다.
const matterResult = matter(fileContents);
// remark()와 remark-html 플러그인을 사용하여 Markdown 내용을 HTML로 변환합니다.
const processedContent = await remark().use(remarkHtml).process(matterResult.content);
// 변환된 HTML을 문자열로 변환합니다.
const contentHtml = processedContent.toString();
// id, 변환된 HTML, 그리고 front matter 데이터를 반환합니다.
return {
id,
contentHtml,
...(matterResult.data as {date: string; title: string; })
}
}
이 posts.ts 모듈은 파일 시스템에 저장된 Markdown 게시물 파일들을 읽어들이고, 파싱하여 웹 페이지에서 사용할 수 있는 형태로 변환하는 역할을 한다. 이는 Next.js의 'Static Generation' 기능을 사용하여 각 게시물 페이지를 미리 렌더링하는 데 필요한 데이터를 제공한다. getSortedPostsData 함수는 게시물 목록 페이지에 사용되며, getAllPostIds와 getPostData 함수는 개별 게시물 페이지를 미리 생성하는 데 사용된다.
[id].tsx 파일에서는 이 posts.ts 모듈의 함수들을 호출하여 필요한 데이터를 가져온다. getStaticPaths 함수에서는 getAllPostIds를 호출하여 미리 렌더링할 모든 경로를 제공하고, getStaticProps 함수에서는 getPostData를 호출하여 선택된 경로에 대한 데이터를 제공한다. 이렇게 가져온 데이터는 Post 컴포넌트에 전달되어 게시물의 제목, 날짜, 내용을 렌더링하는 데 사용된다.
애플리케이션 스타일링
블로그 Home에 표시되는 텍스트들을 가운데로 옮겨주는 스타일링을 적용해보자.
Home.module.css에 다음 스타일링을 추가시켜줘서 index.tsx에 적용시켜주자.
<Home.module.css>
.container {
max-width: 36rem;
padding: 0 1rem;
margin: 3rem auto 6rem;
}
<index.tsx>
그럼 다음과 같이 텍스트들이 옮겨진 것을 확인할 수 있다.
이제 post들도 텍스트들을 가운데로 옮겨주는 스타일링을 적용해보자.
우선 styles 폴더 안에 Post.module.css 파일을 생성해주자.
Home.module.css에 추가한 코드와 똑같이 Post.module.css에도 추가해주자.
<Post.module.css>
.container {
max-width: 36rem;
padding: 0 1rem;
margin: 3rem auto 6rem;
}
그리고 [id].tsx에 import 해주자.
import postStyle from '../../styles/Post.module.css'
그럼 다음과 같이 텍스트들이 옮겨진 것을 확인할 수 있다.
이렇게 해서 Next.js와 TypeScript로 블로그 만들기를 마무리했다!