React

8. Next.js와 TypeScript

기 도 2023. 5. 29. 17:18

 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를 사용해야 할 때

  1. 사용자의 요청보다 build time 에 필요한 데이터를 먼저 가져올 때
  2. Headless CMS 에서 데이터를 가져올 때
  3. 데이터를 공개적으로 캐시할 수 있을 때
  4. 페이지가 미리 렌더링되어야 하고 매우 빨라야할 때

# 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를 사용해야 할 때

  1. 데이터를 가져와야하는 페이지를 미리 렌더해야 할 때
  2. 데이터가 많이 바뀔 때 -> request 를 보낼 때마다 새롭게 데이터를 가져올 수 있게 하기 위해 getServerSideProps 를 사용해준다.

# 서버가 모든 요청에 대한 결과를 계산하고, 추가 구성없이 CDN 에 의해 결과를 캐시할 수 없기 때문에 첫 번째 바이트까지의 시간은 getStaticProps 보다 느리다.

 

 TypeScript 란? 

 TypeScript 가 나오게 된 배경

: JavaScript 는 원래 클라이언트측 언어로 도입되었다.

-> Node.js의 개발로 인해 JavaScript 를 클라이언트측 뿐만이 아닌 서 측 기술로도 활용할 수 있게 만들었다.

-> JavaScript 코드가 커지고 복잡해질수록 코드를 유지, 관리하고 재사용하기가 어려워졌다.

-> 더욱이 Type 검사 및 컴파일 시 오류 검사의 기능을 수용하지 못하기 때문에, JavaScript 는 본격적인 서버 측 기술로 엔터프라이즈 수준에서 성공하지 못하게 된다.

=> 이것을 해결하기 위해 TypeScript가 제시되었다.

 

 TypeScript 란?

: JavaScript에 Type 을 부여한 언어

-> 자바스크립트의 확장된 언어라고 볼 수 있다.

-> TypeScript 는 JavaScript 와 달리 브라우저에서 실행 하려면 파일을 한번 변환해주어야 한다.

# 이 변환 과정을 컴파일이라고 부른다.

 

 Type System 이란?

: 개발 환경에서 에러를 잡는 걸 도와준다.

-> type annotations를 사용해서 코드를 분석할 수 있다.

-> 오직 개발 환경에서만 활성화된다.

-> 타입 스크립트와 성능 향상과는 관계가 없다.

 

 TypeScript 사용하는 이유

  1. JavaScript 코드를 단순화하여 더 쉽게 읽고 디버그할 수 있도록 도와준다.
  2. 오픈 소스이다.
  3. JavaScript IDE 등을 위한 생산적인 개발 도구를 제공해준다.
  4. 일반 JavaScript 의 문제점을 개선할 수 있다.
  5. ES6(ECMAScript 6) 의 모든 이점과 더 많은 생산성을 제공해준다.
  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 파일을 생성한다.

 

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 설치

  1. next app 설치를 위한 폴더를 만든다.
  2. 터미널에 아래 명령어를 입력하여 설치.
 npx create-next-app@latest --typescript ./

 

 백엔드 서비스를 위한 포켓베이스 이용하기

-> 원래 각각의 Post들은 데이터베이스에서 일일히 하나씩 가져왔었다.

=> 하지만 일일이 하나씩 가져오기에는 시간이 너무 오래걸리니까 pocketbase를 이용할 것이다.

https://pocketbase.io/docs/

 

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> &nbsp; &nbsp;
      <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