ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 8. Next.js와 TypeScript
    React 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

    'React' 카테고리의 다른 글

    10. 리덕스  (0) 2023.06.20
    09. 리액트 Version 18  (0) 2023.06.19
    7. React TDD 를 이용한 간단한 앱 생성 및 배포  (0) 2023.05.22
    6. React TDD 기본  (0) 2023.05.22
    5. Netflix 앱 완성하기  (0) 2023.05.10
Designed by Tistory.