[튜토리얼] Next.js 프로젝트 시작하기 5편 (게시글 열람, 수정, 삭제 기능 추가)

2024. 9. 30. 19:56Programming/ReactJS

 

이번 편에서는 CRUD 기능 중에 RUD 부분을 만들어 간다.

순서적으로는 만들기 간단한 R > D > U 순으로 만들 예정이다.

 

1. 글 열람 기능 만들기

 

/dashboard/{게시글id}

로 접속하면 글 열람 화면이 나오게 하면 된다.

 

1) 위 URL 경로에 따라 페이지를 만든다.

2) 글 열람 API를 만든다.

3) 경로에 접속해 페이지가 표시될 때 글 열람 API를 호출한다.

4) 호출해 받은 데이터를 페이지에 바인딩할 수 있게 HTML 부분을 만든다.

 

대략 이 순서대로 하나씩 만들어 가면 된다.

일단 기존에 했던 대로 폴더명을 경로에 맞게 지정하고 안에 page.tsx 파일을 만든다.

 

dashboard/[id]/page.tsx

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import pageStyles from './read.module.scss';
import DeleteButton from './deleteButton';

export default function Read({ params }: { params: { id: string } }) {
    const [title, setTitle] = useState('');
    const [content, setContent] = useState('');
    const [author, setAuthor] = useState('');
    const router = useRouter();

    return (
      <div className={pageStyles.container}>
        <h1 style={{ textAlign:"center"}}>글 보기</h1>
        <form className={pageStyles.form} >
          {/* 제목 입력 */}
          <div className={pageStyles.field}>
            <label htmlFor="title">제목:</label>
            <input
              type="text"
              id="title"
              value={title}
              className={pageStyles.input}
              onChange={(e) => setTitle(e.target.value)}
              readOnly
            />
          </div>

          {/* 글쓴이 입력 */}
          <div className={pageStyles.field}>
            <label htmlFor="author">글쓴이:</label>
            <input
              type="text"
              id="author"
              value={author}
              className={pageStyles.input}
              onChange={(e) => setAuthor(e.target.value)}
              readOnly
            />
          </div>
  
          {/* 내용 입력 */}
          <div className={pageStyles.field}>
            <label htmlFor="content">내용:</label>
            <textarea
              id="content"
              value={content}
              className={pageStyles.textarea}
              onChange={(e) => setContent(e.target.value)}
              readOnly
            />
          </div>
  
          {/* 버튼 그룹 */}
          <div className={pageStyles.button_group} >
            <button className={pageStyles.button} onClick={(e) => { e.preventDefault(); router.push('/dashboard')}}>취소</button>
            <div className={pageStyles.right_btn}>
                <button type="submit" className={pageStyles.button} onClick={(e) => { e.preventDefault(); router.push(`/dashboard/edit/${params.id}`)}}>수정</button>
                <DeleteButton params={{ id: params.id }}/>
            </div>
          </div>
        </form>
      </div>
    );
  }

 

저번에 만들었던 새 글 작성 화면을 재활용해 만든 것이다.

여기까지 완성이 되었다면 다음과 같은 화면이 표시될 것이다.

 

 

기존 input 태그를 그대로 활용하고 싶어 readOnly 속성을 넣고,

테두리를 표시하지 않도록 스타일을 조금 변경했다.

그리고 취소 버튼을 누르면 /dashboard로 리다이렉트되도록 했다.

 

그 다음으로는 글 열람 API를 만들었다.

경로와 이름만 잘 신경 써서 만들면 된다.

 

/api/posts/[id]/route.ts

import { NextResponse, NextRequest } from 'next/server';
import { sql } from '@vercel/postgres';

export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
  try {
    const { rows } = await sql`
      SELECT id, title, content, name, created_at
      FROM board
      WHERE id = ${params.id}
      ORDER BY created_at DESC;
    `;
    return NextResponse.json(rows[0], { status: 200 });
  } catch (error) {
    console.error('Error fetching posts:', error);
    return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 });
  }
}

 

API가 완성되면 호출 부분을 만들어준다.

앞서 만든 dashboard/[id]/page.tsx 파일에 API 호출부를 추가한다.

useEffect를 사용해 페이지가 마운트된 후 한 번만 API가 호출되게 설정했다. 

useEffect(() => {
        fetch(`/api/posts/${params.id}`)
        .then((res) => res.json())
        .then((data) => {
            setTitle(data.title);
            setContent(data.content);
            setAuthor(data.name);
        }).catch((error) => {
            console.error('Error fetching posts:', error);
        });
    }, []);

 

여기까지 완료되면 실제 DB에서 가져온 데이터가 글 열람 화면에 표시될 것이다.

 

2. 글 삭제 기능 만들기

앞서 글 열람 기능 만들던 방식과 순서는 동일하다.

 

1) 버튼 컴포넌트를 만든다.

2) 글 삭제 API를 만든다.

3) 버튼을 누르면 글 삭제 API가 실행되도록 한다.

4) 글 삭제가 성공하면 /dashboard 화면으로 리다이렉트한다.

 

3. 글 수정 기능 만들기

글 수정 기능은 글 삭제 기능보다는 만들 때 생각해야 할 게 좀 더 많다.

기존에 만든 화면을 어디까지 재활용할 것인지, 글 수정 API 경로는 어떻게 설정할 것인지,

HTTP 메소드는 GET/POST/PUT/DELETE 중 어떤 것을 사용할 것인지.

만들기 전에 가장 중요한 것은 어떤 방식으로 기능이 작동될지 구체적인 이미지를 갖고 만드는 것이다.

 

1) 수정 버튼 클릭시 수정 화면으로 이동한다.

2) 수정 화면에서 취소 버튼을 누르면 열람 화면으로 리다이렉트한다.

3) 수정 화면에서 수정 버튼을 누르면 글 수정 API (PUT 메소드)를 호출한다.

 

위처럼 구체적인 이미지를 가진 상태에서 한 부분씩 만들어 간다.

너무 고심할 필요는 없이 뭔가 이상하다 생각하면 나중에 고쳐 나가도 된다.

 

/dashboard/edit/[id]/page.tsx

'use client';

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import pageStyles from './edit.module.scss';

export default function Edit({ params }: { params: { id: string } }) {
    const [title, setTitle] = useState('');
    const [content, setContent] = useState('');
    const [author, setAuthor] = useState('');
    const router = useRouter();
    console.log(params);

    useEffect(() => {
        fetch(`/api/posts/${params.id}`)
        .then((res) => res.json())
        .then((data) => {
            setTitle(data.title);
            setContent(data.content);
            setAuthor(data.name);
        }).catch((error) => {
            console.error('Error fetching posts:', error);
        });
    }, []);

    const handleSubmit = async (e: React.FormEvent) => {
      e.preventDefault();
  
      // 수정된 데이터를 서버로 전송
      const response = await fetch(`/api/posts/${params.id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ title, content, author }),
      });
  
      if (response.ok) {
        setTitle('');
        setContent('');
        setAuthor('');
        router.push('/dashboard'); // 수정 후 대시보드로 이동
      } else {
        console.error('Failed to update post');
      }
    };

    return (
      <div className={pageStyles.container}>
        <h1 style={{ textAlign:"center"}}>글 보기</h1>
        <form className={pageStyles.form} >
          {/* 제목 입력 */}
          <div className={pageStyles.field}>
            <label htmlFor="title">제목:</label>
            <input
              type="text"
              id="title"
              value={title}
              className={pageStyles.input}
              onChange={(e) => setTitle(e.target.value)}
            />
          </div>

          {/* 글쓴이 입력 */}
          <div className={pageStyles.field}>
            <label htmlFor="author">글쓴이:</label>
            <input
              type="text"
              id="author"
              value={author}
              className={pageStyles.input}
              onChange={(e) => setAuthor(e.target.value)}
            />
          </div>
  
          {/* 내용 입력 */}
          <div className={pageStyles.field}>
            <label htmlFor="content">내용:</label>
            <textarea
              id="content"
              value={content}
              className={pageStyles.textarea}
              onChange={(e) => setContent(e.target.value)}
            />
          </div>
  
          {/* 버튼 그룹 */}
          <div className={pageStyles.button_group} >
            <button className={pageStyles.button} onClick={(e) => { e.preventDefault(); router.push('/dashboard')}}>취소</button>
            {/* 제출 버튼 */}
            <button type="submit" onClick={handleSubmit} className={pageStyles.button}>수정</button>
          </div>
        </form>
      </div>
    );
  }

 

완성된 수정 화면 기준으로는 이렇게 되는데 몇 가지 주의점을 살펴 보자.

 

1) URL 파라미터값 (게시글 id)를 기준으로 열람 API를 호출해 새로 데이터를 가져온다.

파라미터를 열람 화면에서 수정 화면으로 보내는 방식도 가능하지만,

그렇게 할 경우 주소창에서 id 직접 변경할 경우 URL과 다른 값이 표시되게 된다.

 

2) 글 수정 API 주소가 /api/posts/{게시글id}로 설정하여 열람 API 주소와 같다.

열람 API는 GET 메소드를 사용하고 수정 API는 PUT 메소드를 사용하므로 어차피 겹칠 일이 없다.

사용하는 API 주소를 계속 늘리게 되면 관리가 어렵기 때문에 이러한 방식을 사용했다.

/api/posts/edit/{게시글id}로 지정하는 것도 가능하기는 하다.

 

 

/api/posts/[id]/route.ts

export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
  const id = params.id;
  const { title, content, author } = await request.json();

  if (!id) {
    return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
  }

  try {
    // 기존 데이터 업데이트
    await sql`
      UPDATE board
      SET title = ${title}, content = ${content}, name = ${author}
      WHERE id = ${id};
    `;

    return NextResponse.json({ message: `Post with ID ${id} has been updated` });
  } catch (error) {
    console.log(error);
    return NextResponse.json({ error: 'Failed to update post' }, { status: 500 });
  }
}

 

이렇게 만든 API를 호출하는 부분을 만들고 호출하고,

호출이 성공했을 경우 어느 페이지로 이동시킬지만 정하면 된다.

 

이것으로 글 열람, 삭제, 수정까지 완료되었다.

기본적인 게시판의 CRUD 기능까지는 모두 끝났다.

하지만 이것으로 모든 작업이 완료된 것은 아니다.

 

배보다 배꼽이 크다는 말처럼 개발이라는 일은 이슈 처리가,

구현보다도 중요하기도 하고 시간도 더 많이 소요되는 경우가 많다.

작업을 하면서 몇 가지 크리티컬한 이슈가 발생한 것을 확인했다.

 

1) 게시글 리스트 화면에서 강제 새로고침하지 않으면 데이터 갱신이 되지 않는 현상

2) 게시글 리스트 화면, 열람 화면에 접근했을 때 API가 2번씩 호출되는 현상 

3) 열람 화면에 접근한 후에 조회수가 늘어나지 않는다 (아직 기능을 만들지 않았다)

4) 중복되는 코드가 꽤 있다 (특히 스타일 부분에서)

 

다음 편에서는 이러한 이슈를 점검하고 수정하는 작업을 진행할 예정이다.