[React] NextJS13

노유성·2023년 5월 29일
0
post-thumbnail

☀️들어가며

NextJS 13 버전은 이전 버전인 NextJS 12와는 다른 점이 있어 이 점을 알아보고자 간단한 웹사이트를 만들었다.

홈페이지는 위와 같이 nav바와 메인 화면으로 구성되어있다.

nav바에서 posts link를 클릭하면은 Post들을 보여주는 게시판이 나오고 각 포스트를 클릭하면 포스트의 상세 정보에 대해서 보여주는 웹사이트이다.

⭐️PocketBase

게시판에 작성될 게시글들의 데이터를 저장하기 위한 백엔드 서버가 있어야 한다. sqlite를 이용하는 백엔드 서버를 간단하게 구성해주는 pocketbase를 이용해서 백엔드 서버를 구성했다.

🪐PocketBase란?

PocketBase는 사용자가 복잡한 데이터베이스 설정 및 관리 작업을 거치지 않고도, 데이터베이스를 빠르게 설정하고 사용할 수 있게 해줍니다. 사용자는 PocketBase 웹 사이트를 통해 몇 가지 간단한 단계를 따라 데이터베이스를 생성하고 구성할 수 있습니다. PocketBase는 기본적인 데이터베이스 관리 작업, 백업 및 복원 등을 자동으로 처리해주기 때문에 개발자는 주로 애플리케이션 개발에 집중할 수 있습니다.
-chatGPT

🪐다운로드 및 설정

https://pocketbase.io/docs/

공식 홈페이지에서 운영체제에 맞는 파일을 다운로드 받은 후 압축을 해제하면 pocketbase.exe 파일이 있다. 해당 파일을 프로젝트 폴더의 루트에 올려놓고 ODS창에

pocketbase serve

명령어를 입력하면은 pocketbase 서버가 열린다.

열린 서버로 들어가면 좌측 상단에서 table을 추가할 수 있다.
New collection을 누르면 다음과 같은 화면이 나오는데


New field를 클릭해서 column을 추가해주면 된다.

예제에서는 posts talbe에 title이라는 field를 만들어서 이용했다.

⭐️Route 시스템

NextJS13 부터는 폴더 기반으로 바뀌었기 때문에 파일 라우팅은 불가능하다. 또한 예약된 파일명 들이 있다.


위 7가지 파일명들은 예약된 파일명으로 Layout은 해당 파일이 포함되어있는 폴더에서의 레이아웃을 담당한다. 포함되어 있는 폴더 뿐만 아니라 하위에 존재하는 폴더(페이지)에 대해서도 레이아웃을 보여준다.

Page에 대해서 설명하기에 앞서 NextJs13에서 라우트를 하는 방법에 대해서 알아보자면
업로드중..
루트 폴더인 app폴더 하위에 위와 같이 파일이 구성되어있다고 했을 때 about/page.js에 선언되어 있는 컴포넌트(페이지)는 해당 tsx파일이 포함되어 있는 폴더의 폴더명으로 라우팅이 된다.

프로젝트 폴더를 자세히 보면은 소괄호 안에 폴더명이 적혀있는 것도 볼 수 있는데 소괄호로 만들어진 폴더는 route 경로에 포함되지 않는다. 또한 동적인 경로를 만들기 위해서는 [slug] 폴더명을 만들어준다.
업로드중..

다른 예약된 파일명들은 웹을 만들면서 알아보자.

⭐️레이아웃

🪐layout.tsx

import Link from 'next/link'
import './globals.css'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body> {children}</body>
    </html>
  )
}

기본적으로 처음 Nextjs 프로젝트 폴더를 생성하면 만들어지는 Layout.tsx파일의 구조이다. children을 인자로 받아서 children을 렌더링한다. children은 해당 Layout폴더가 포함되어 있는 파일 및 하위 파일들의 요소들을 의미한다.

🪐nav바 만들기

import Link from 'next/link'
import './globals.css'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <nav>
          <Link href="/">
            Home
            </Link> &nbsp; &nbsp;
          <Link href="/posts">
            Posts
          </Link>
        </nav>
        <main>
          {children}
        </main>
      </body>
    </html>
  )
}

기존에 만들어진 Layout.tsx파일에 nav바를 추가하고 nav파에는 각각 루트화면으로 갈 수 있는 Link 태그와 /posts로 갈 수 있는 Link 태그를 만들었다. 그리고 기존에 있었던 내용은 main 태그 안으로 넣어주었다.

⭐️Page

🪐/page.tsx

import React from 'react'

export default function HomePage() {
  return (
    <div>HomePage</div>
  )
}

루트 경로에 보여주는 메인페이지이다.

🪐/posts/page.tsx

import Link from 'next/link';
import React from 'react'
import CreatePost from './[id]/CreatePost';

async function getPost() {
  const res = await fetch('http://127.0.0.1:8090/api/collections/posts/records', 
                          {cache: 'no-store'});
  const data = await res.json()
  return data?.items as any[];
}

const PostsPage = async () => {
  const posts = await getPost();

  return (
    <div>
      <h1>Posts</h1>
      {posts?.map((post) => {
        return <PostItem key={post.id} post={post} />
      })}
      <CreatePost/>
    </div>
  )
}

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

☄️PostPage

먼저 PostPage부터 보면은 UI를 구성하는 컴포넌트이다. getPost()함수를 이용해서 post들을 배열로 받아온다. optional chaning을 이용해서 만약에 posts 변수가 null, undefined가 아니라면 map method를 돌면서 PostItem 컴포넌트를 생성한다. 그리고 나중에 작성할 새로운 post를 생성할 form인 CreatePost 컴포넌트도 있다.

☄️PostItem

post를 인자로 받아서 post의 property를 distructuring으로 가져온다. 그리고 해당 값들을 이용해서 UI를 작성한다. UI 요소를 클릭하면 세부정보를 보여줄 것이기 때문에 Link 태그를 이용해서 route를 해주었으며 dynamic한 경로로 설정되어 있다. 추후에 해당 경로에 해당하는 [id]폴더를 만들어서 처리를 해줄 예정이다.

☄️getPost

PocketBase에 데이터를 요청하는 부분이다. fetch 함수를 이용해서 가져오고 두번째 인자로는 가져온 데이터를 어떻게 처리할 것인가에 대한 내용을 담을 수 있다. 기본적으로 default가 가져온 데이터를 캐시하는 것인데 {cache: no-store}을 인자로 넘겨줌으로써 캐시를 막을 수 있다.

🪐/posts/[id]/page.tsx

import React from 'react'

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('faild to fetch data'); // 에러가 발생하면은 페이지에서 가장 가까이에 있는 error.tsx 파일을 찾아서 실행함
  }
  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

☄️PostDetailPage

post의 세부정보를 보여주는 페이지의 UI를 담당한다. 인자로 params를 받는다. 동적 세그먼트의 값의 params에 id값이 있다. 해당 id값을 인자로 주어 getPost()함수를 호출하고 ㅂ다아온 값으로 UI를 보여준다.

☄️getPost

해당 post의 데이터를 가져오는 함수이다. 인자로 id를 받은 다음 id를 기준으로 api를 이용해 데이터를 요청하고 받아와서 반환한다. 만약에 에러가 발생하면 에러를 던진다.

던져진 에러는 가장 가까운 error.tsx 가 받아서 처리한다.

🪐/posts/[id]/CreatePost.tsx

'use client';
import { useRouter } from 'next/navigation';
import React, { useState,  } from 'react'

export default function 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={(e) => handleSubmit(e)}>
      <input type="text"
        placeholder='TItle'
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <button type="submit">
        Create Post
      </button>
    </form>
  )
}

포스트를 새로 생성하는 부분을 담당하는 컴포넌트이다. 컴포넌트를 설명하기에 앞서 nextjs에서는 client component와 server component가 있는데 nextjs에서는 기본적으로 server component가 이용된다. 그러므로 만약에 client component를 이용하고 싶다면 맨 윗줄에 'use client'를 기입하면 된다.

UI는 기본적으로 input 태그에 데이터를 작성하고 제출하기 위한 구성이 되어있으며 주목할 부분은 handleSubmit이다.

☄️handleSubmit

submit event가 발생하면 동작하는 핸들러이다. fech함수를 이용해서 post를 생성한다. 그리고 화면을 새로고침하기 위해서 refresh를 해준다.

화면이 refresh를 하면은 전체 페이지가 refresh되는 것이 아니라 현재 파일이 포함된 경로와 하위 경로만 refresh가 된다.

🪐/posts/[id]/DeletePost.tsx

포스트를 삭제하는 버튼 컴포넌트이다.

'use client';
import { useRouter } from 'next/navigation';
import React from 'react'

const DeletePost = ({ id }: { id: string }) => {
  const router = useRouter();

  const handleDelete = async (e: any) => {
    e.preventDefault();

    await fetch(`http://127.0.0.1:8090/api/collections/posts/records/${id}`,
      {method: "DELETE"});

    router.refresh();
  }

  return (
    <button onClick={(e) => handleDelete(e)}>
      x
    </button>
  )
}

export default DeletePost

클릭을 하면은 클릭한 버튼이 포함된 포스트의 id값을 인자로 받아서 fetch 함수를 이용해 삭제하는 리스너가 포함된 버튼 component를 반환한다.

profile
풀스택개발자가되고싶습니다:)

0개의 댓글