콤퓨우터/프로그래밍

Next.js Full Course 필기 (1)

파란화면 2024. 3. 17. 16:14
반응형

 

옛날에 위의 영상을 보고 공부했던 내용을 개인 필기용으로 적어둔 것입니다

File System Routing

/app/about example.com/about

  • Dynamic Route
    /app/[slug] example.com/{slug}
    • Catch-All Route
      /app/[...id] example.com/…id/…id/…id
  • ()
    /app/(group) - will be ignored by routing system

 

Reserved Filenames

  • page.tsx (in TS) or page.js
    실제 UI를 정의하는 기본 React 컴포넌트를 export
  • layout.tsx (in TS) or layout.js
    UI that surrounds the entire application
  • route.tsx (in TS) or route.tsx - Route Handler
    • JSON 등을 return하는 데에 사용할 수 있음, page와 같은 디렉토리에 사용할 수 없음

 

Route Handler

  • HTTP 메소드들(GET, POST, PUT...)와 같은 이름을 가지는 함수를 1개 이상 export 할 수 있음
  • 각 함수는 들어오는 request에 대한 정보를 제공하는 request parameter를 가짐
  • 해당 request를 처리하기 위해 함수에서 response를 return할 수 있음
  • 예시: POST로 form을 받아서 뭔가 한 뒤 Response Body 'we did it'을 반환 
    • Route Handler들은 언제나 서버사이드에서 실행되며 기본적으로 Node.js 런타임에서 실행
      • export const runtime으로 변경 가능
export async function POST(request: Request) {
	const data = await request.json();
	//do-something
	return new Response('we did it');
}

 

Request & Response API

  • 있으면 삶이 편해지는 기능들을 제공
  • 예를 들어 Response로 JSON을 보내고 싶다든가 할 때에 유용하다 
import { NextRequest, NextResponse } from 'next/server';
export async function PATCH(request: NextRequest) {
	const url = request.nextUrl;
	return NextResponse.json({message: "asdf"});
}

 

Layouts

  • 기본적으로 /app/layout.tsx가 있어서, 전 애플리케이션에 있어서의 outer UI 루트 레이아웃을 정의함
  • 레이아웃은 페이지와 비슷하지만, 말하자면 자식들에게 상속되는 레이아웃임
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode
    }) {
      return (<html>
          <body>
              {children}
          </body>
      </html>
      );
    }
  • 레이아웃은 Nest될 수 있음
  • 레이아웃에서도 fetch()로 FETCHING 이 가능
  • layout group (괄호가 들어간 폴더)와 결합하여 쓸 수도 있음?
  • 레이아웃의 UI와 상태는 route 변화되더라도 계속 유지됨
    • 예를 들어, 레이아웃의 return값에 <NavMenu/> 가 포함되어 있다면, 해당 레이아웃이 사용되는 모든 페이지에서 <NavMenu/>는 재렌더링되지않음
    • 레이아웃 컴포넌트를 매 내비게이션 때마다 reinitalise하려면 template.tsx를 이용할 수 있음
      • which re-mounts on route change

 

Server Component

Handle Server-Side Rendering for SEO

  • 전통적으로, Next.js는 SSR, ISR, SSG와 같은 다양한 종류의 렌더링 기법들을 대응해왔음
  • 하지만 Next.js 13에서는, 모든 페이지는 Server Component이다
    • 서버에서 렌더링된다 (클라에게 HTML을 쏜다)
    • 따라서, React의 useEffect()와 같은 Client-Side 코드들을 그대로 쓸 수 없다
  • 최상단에 'use client'; 로 선언되는 클라이언트 컴포넌트를 쓰면 클라이언트 사이드 코드들을 쓸 수 있다
    • useEffect() 같은 걸 쓰고 싶으면 이쪽으로 옮겨야
  • 자동으로 캐시된다
  • 보통 캐시 관련 옵션은 Next.js가 알아서 하지만 유저가 행동을 바꿔줄수도 있다
    • export const dynamic = ''; 변수를 선언하여 behaviour를 변경할 수 있다
      • force-dynamic : SSR wo/ caching (매 호출시마다 서버에서 렌더링)
      • force-static: SSG, 무조건 페이지를 캐시
    • export const revalidate = intval 을 선언하여 몇초 간 캐시된 내용을 썼다가 시간 경과 후 재렌더링되도록 할 수도 있다 (ISR-equiv)
  • SEO 관련: export const metadata = {} 를 선언하여 HTML <meta> 태그의 내용을 넣을 수 있다

 

Data Fetching

  • Next.js 13에서 모든 레이아웃과 페이지는 서버 컴포넌트이므로, 서버 사이드 리소스(Environment Variables 등)와 데이터베이스에 접근할 수 있다
    • 구버전에서처럼 getServerSideProps(), getStaticProps() 써서 컴포넌트에 props를 넘겨주며 삽질할 필요가 없다
    • 서버 사이드 컴포넌트는 async await를 이용해 내부에서 직접적으로 data fetching을 할 수 있다.
      • primsa? async 컴포넌트 안에서 await prisma.getMany(); 같이 쓰면 된다.
      • firebase? async 컴포넌트 안에서 await firebase.getDoc(); 처럼 쓰면 된다.
      • JS fetch()? async 컴포넌트 안에서 await fetch(); 해 주면 된다.
  • 개발이 편해지는 것은 물론, nested component 구조에서는 fetching에 대해 병렬 처리가 이루어지므로 종전 구조 대비 성능도 향상된다

 

  • 사실 Next에서 쓰게 되는 fetch()는 바닐라 JS의 그것이 아니라, React에서 확장해 놓은 fetch()이다
    • 이것은 Automatic Request Deduping에 대응한다 (여러 컴포넌트에서 중복 Fetch 요청 시, 한 번 fetch해서 받은 데이터를 중복 요청한 곳에 다시 갖다줌)
    • 또한 cache 프로퍼티를 이용해 캐시 동작을 정의해줄 수도 있다
      • static한 데이터라면 {cache: 'force-cache'}로 캐시 강제
      • 항상 바뀌는 데이터라면 {cache: 'no-store'}
      • 그 중간이라면 revalidate 옵션을 넣어 캐시 만료기간을 정해줄 수 있다

Fetching data from PocketBase

이 강의에서 사용한 PocketBase는 built-in REST API를 가지고 있는, 가제트 만능 단일 바이너리 데이터베이스이다.

async function getNotes() {
    const res = await fetch('http://127.0.0.1:8090/api/collections/notes/records?page=1&perPage=30', { cache: 'no-store' });
    //를 하면 notes 컬렉션(테이블)에서 30개 단위로 페이지네이션된 레코드를 던져준다.
    const data = await res.json();

    return data?.items as any[];
    //데이터베이스에 있는 데이터의 Array.
}

export default async function NotesPage() {
    const notes = await getNotes();
  • PocketBase REST API는 이런식으로 return한다:
    {
      "page": 1,
      "perPage": 30,
      "totalItems": 1,
      "totalPages": 1,
      "items": [
          {
              "collectionId": "n6nglbveywsg98j",
              "collectionName": "notes",
              "content": "hello",
              "created": "2023-11-10 05:18:34.497Z",
              "id": "r4ctus4mcew6cdg",
              "title": "hello world",
              "updated": "2023-11-10 05:18:34.497Z"
          }
      ]
    }

Server-Rendered이지만 이 Route는 자동으로 캐시된다, Route Segment가 Dynamic이 아니기 때문 (Static Page처럼 취급된다)

때문에 fetch에 , { cache: 'no-store' }를 넣어줘야함
이러면 매 Request마다 아이템을 refetch한다

 

Dynamic Route: 노트의 제목

http://127.0.0.1:3000/notes/r4ctus4mcew6cdg 와 같은 URL이 노트 상세페이지를 가리키게 하자

  • /app/notes/[id]/page.tsx 생성
  • 대충 위와 비슷한 소스를 생성한다
async function getNote(noteId: string) {
    const res = await fetch(`http://127.0.0.1:8090/api/collections/notes/records/${noteId}`, {next:{revalidate: 10}});
    const data = await res.json();
    return data;
}

export default async function NotePage({params}: any) {
    const note = await getNote(params.id);
    return(
        <div>
            <h1>notes</h1>
            <div>
                <h3>{note.title}</h3>
                <h5>{note.content}</h5>
                <p>{note.created}</p>
            </div>
        </div>
    );
}
  • fetch()에서 cache:'no-store'를 넣을 필요는 없는데, Dynamic Route이기 때문에 매 Request마다 fetch하는 것이 default이기 때문
    • 하지만, next: {revalidate: 10}과 같은 옵션을 넣어, ISR (Incremental Static Regeneration) 을 구현할 수 있음
      • 캐싱된 페이지가 10초보다 낡았으면 페이지를 재생성

로딩 화면

loading.tsx 파일 생성

 

"Interactive"한 CreateNote component

  • 'use client'; : 서버에서 렌더링하지 않고 브라우저에서 렌더링
  • React의 useState Hook을 이용해 title, content에 대한 field를 추가

 

  • 잠깐 - 리액트 톺아보기: 
    • Hook: 리액트 프레임워크의 다양한 기능을 사용하기 위하여 컴포넌트의 TOP LEVEL에서 사용될 수 있는 함수
    • useState() 훅을 사용하면 Stateful한 Value를 만들어, 변경될 때마다 여기에 의존하는 컴포넌트들이 자동으로 재렌더링되게할수있다.
    • e.g., const [title, setTitle] = useState('');
      • 기본값을 ''으로, 반환하는 것은 [title, setTitle]로 구성된 Array
      • 배열의 첫 번째는 UI에서 사용할 실제 값으로, 두 번째는 함수로 구성한다
  • onChange 때마다 setTitle(), setContent()를 각기 불러 state를 update

 

'use client';
import { useState } from "react";
export default function CreateNote() {
    const [title, setTitle] = useState('');
    const [content, setContent] = useState('');

    const create = async() => {
        await fetch('http://127.0.0.1:8090/api/collections/notes/records',
            {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({title, content})
            }
        );
    }

    return(
        <form onSubmit={create}>
            <h3>create a new note</h3>
            <input
                type="text" placeholder="title" value={title}
                onChange={(e) => setTitle(e.target.value)}
            />
            <textarea
                placeholder="content" value={content}
                onChange={(e) => setContent(e.target.value)}
            />
            <button type="submit">create note</button>
        </form>
    );
}
  • create 내장-함수 만들기: 클라의 폼 내용을 PocketBase REST API에 fetch()

새로고침을 하지 않아도

import { useRouter } from "next/navigation";

...
router.refresh();

 

Streaming

  • 일반적으로 Next 웹페이지 렌더링은 다음의 서순을 따른다
  1. 서버에서 데이터를 fetch한다
  2. React 컴포넌트를 서버에서 HTML로 렌더링한다
  3. 서버는 HTML을 브라우저에 보낸다
  4. 브라우저는 HTML/CSS를 렌더링한다 (Non-Interactive Page)
  5. 브라우저에서 JS가 실행되어 Hydrated되며 Interactive한 페이지가 완성된다
  • 이 과정은 순차적으로, 데이터를 많이 fetch해야 하는 큰 웹사이트에서는 많은 데이터를 로드해야할 수 있고 그러면 사이트가 느려진다
  • Next.js 13에서는 페이지를 컴포넌트별로 조각조각 쪼개서 페이지를 프로그레시브하게 로드한다
    • 이것을 페이지 스트리밍이라고 하는데 사실 Next가 알아서 하는 부분이기는 한다
    • 하지만 loading.tsx와 같은 파일을 라우트에 추가하여 UX를 향상시킬 수 있다
      • 이러면 다른 컴포넌트가 로딩되는 사이에 loading.tsx의 내용이 표시된다
  • 이 알잘딱의 비결은 Suspense이다
  • React에서, Suspense는 suspense boundary를 생성하는 특수 컴포넌트이다
    • 데이터 fetching같이 async한 동작을 하는 컴포넌트를 감싸주고, async operation이 끝날 때까지 fallback UI를 표시한다

 

Auth.js를 통한 로그인

  • 기본적으로 jwt (JSON Web token) - 암호화된 토큰을 클라이언트 사이드에 저장
    • 데이터베이스에 저장할 수도 있음
  • 알아서 로그인 시나리오를 커버해주는 다양한 API Route를 생성
  • 이후 Application의 root에 <SessionProvider>를 추가
export default function AuthProvider({children}: Props) {
    return <SessionProvider>{children}</SessionProvider>;
}
  • 모든 자식들은 useSession() Hook을 이용해 사용자에 대한 갱신사항을 리얼타임으로 들을수있음
'use client';
import { useSession } from 'next-auth/react';
export default function AuthCheck({children} : {children: React.ReactNode}) {
	const {data: session, status} = useSession();
}
  • 또한 로그인, 로그아웃을 위한 함수도 제공됨. signIn() 의 경우 버튼에 바운드시켜주면 전용 페이지로 이동
export function SignOutButton() {
	return <button onClick{() => signOut()}>Sign Out!</button>;
}
  • 서버 사이드에서는, getServerSession() 훅을 이용해서 로그인 상태를 받을 수 있음
반응형