본문 바로가기
프로젝트 기록/React, Node.js를 이용한 게시판 만들기

2. React와 Node.js를 사용한 게시판 웹 애플리케이션 만들기 - CRUD API 구현, 게시글 수정과 삭제 이벤트, Endpoint

by jeong11 2024. 11. 14.
반응형

Todo
구현할 기능 : CRUD (게시글 작성, 조회, 수정, 삭제)


<RESTful API 이해하기>
https://tiny-immj.tistory.com/88

 

RESTful API 간단한 API 만들기, Postman 사용법

~ 포스팅 진행 순서  ~REST, REST APIRESTful설계원칙API 구현하기=> REST, REST API, RESTful의 기본 개념과 간단한 api 만드는 방법 진행=> RESTful API는 데이터를 표준화된 방식으로 주고 받을 수 있게 해주고,

tiny-immj.tistory.com

 
 

프로젝트 정보 및 CR(게시글 작성, 조회)

 
<1. React와 Node.js를 사용한 게시판 웹 애플리케이션 만들기 - 게시판 만들기, 게시글 목록 자동 갱신, 글 추가, Router 설정, CORS설정, MySQL>
https://tiny-immj.tistory.com/92

 

1. React와 Node.js를 사용한 게시판 웹 애플리케이션 만들기 - 게시판 만들기(crud), 게시글 목록 자동

Todo구현할 기능 : CRUD(게시글 작성, 조회, 수정, 삭제) RESTful API 이해하기https://tiny-immj.tistory.com/88 REST, REST API, RESTful의 기본 개념과 간단한 api 만드는 방법 진행=> RESTful API는 데이터를 표준화된 방

tiny-immj.tistory.com


 

프로젝트 정보

프론트엔드 : React
백엔드 : Node.js
<Node.js 세팅>
https://tiny-immj.tistory.com/78

 

React, node.js를 이용한 기초 프로젝트 생성(for mac)

1. 개발 세팅 준비1-1. Node.js 설치하기 https://nodejs.org/ Node.js — Run JavaScript EverywhereNode.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.nodejs.org  pkg 파일이 다운받아지면 실행시켜 설치해주세

tiny-immj.tistory.com

DB : MySQL
<DB 세팅>
https://tiny-immj.tistory.com/79

 

1. React, node.js를 사용한 게시판 만들기 - db 세팅

이전에 node.js, npm, git 세팅을 하고 기초 프로젝트를 만들었다https://tiny-immj.tistory.com/78 React와 node.js를 이용한 기초 프로젝트 생성(for mac)1. 개발 세팅 준비1-1. Node.js 설치하기 https://nodejs.org/ Node.j

tiny-immj.tistory.com

 
 
 

1. Backend 백엔드

1-1. 게시판 API 구현

1) 게시판 API 
> server.js
지난 포스팅때 설정한 게시판 API

const express = require('express');
const cors = require('cors');  // CORS 패키지 추가
const db = require('./db');
const app = express();

app.use(cors());  // CORS 미들웨어 추가
app.use(express.json()); // JSON 요청을 처리하기 위해 필요

// 게시글 목록 조회 API
app.get('/api/posts', (req, res) => {
  const query = 'SELECT * FROM posts';
  db.query(query, (err, results) => {
    if (err) {
      return res.status(500).send('Error fetching posts');
    }
    res.json(results);
  });
});

// 게시글 작성 API
app.post('/api/posts', (req, res) => {
  const { title, content } = req.body;
  const query = 'INSERT INTO posts (title, content) VALUES (?, ?)';
  db.query(query, [title, content], (err, result) => {
    if (err) {
      return res.status(500).send('Error creating post');
    }
    res.status(201).send('Post created successfully');
  });
});

// 게시글 상세 정보 가져오기
app.get('/api/posts/:id', (req, res) => {
  const { id } = req.params;
  db.query('SELECT * FROM posts WHERE id = ?', [id], (err, results) => {
    if (err) {
      console.error(err);
      return res.status(500).json({ message: '서버 오류' });
    }
    if (results.length === 0) {
      return res.status(404).json({ message: '게시글을 찾을 수 없습니다' });
    }
    res.json(results[0]);
  });
});

// 게시글 수정 API
app.put('/api/posts/:id', (req, res) => {
  const { id } = req.params;
  const { title, content } = req.body;
  const query = 'UPDATE posts SET title = ?, content = ? WHERE id = ?';
  db.query(query, [title, content, id], (err, result) => {
    if (err) {
      return res.status(500).send('Error updating post');
    }
    res.send('Post updated successfully');
  });
});

// 게시글 삭제 API
app.delete('/api/posts/:id', (req, res) => {
  const { id } = req.params;
  const query = 'DELETE FROM posts WHERE id = ?';
  db.query(query, [id], (err, result) => {
    if (err) {
      return res.status(500).send('Error deleting post');
    }
    res.send('Post deleted successfully');
  });
});

// 서버 실행
const PORT = 5001;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

 
 
2) API와 엔드포인트의 개념 설명과 차이점

API(Application Programming Interface) : 서로 다른 소프트웨어/시스템이 상호작용할 수 있도록 하는 프로토콜의 모음 
Endpoint : API가 서버에서 리소스에 접근할 수 있도록 가능하게 하는 URL, API가 외부와 데이터를 주고받기 위해 지정된 URL

 
API는 특정 기능을 제공하고 개발자가 서버나 서비스에 접근해 필요한 작업을 수행할 수 있도록 도와준다
예를 들어 "게시물을 생성하는 API", "사용자 정보를 가져오는 API"가 있다
→ 기능의 개념 
Endpoint는 API가 실제로 실행되는 주소로, 예를 들어 "게시물을 생성하는 API"의 엔드포인트가 /posts/create라면 이 주소를 통해 클라이언트가 서버에 요청을 보내게 된다
 
 

1-2. 게시글 수정 API

수정 API 엔드포인트는 이렇게 만들었다
//server.js

// 게시글 수정 API
app.put('/api/posts/:id', (req, res) => {
  const { id } = req.params;
  const { title, content } = req.body;
  const query = 'UPDATE posts SET title = ?, content = ? WHERE id = ?';
  db.query(query, [title, content, id], (err, result) => {
    if (err) {
      return res.status(500).send('Error updating post');
    }
    res.send('Post updated successfully');
  });
});

 
 

1-3. 게시글 삭제 API 

삭제 엔드포인트는 이렇게 만들어 놓았다 
//server.js

// 게시글 삭제 API
app.delete('/api/posts/:id', (req, res) => {
  const { id } = req.params;
  const query = 'DELETE FROM posts WHERE id = ?';
  db.query(query, [id], (err, result) => {
    if (err) {
      return res.status(500).send('Error deleting post');
    }
    res.send('Post deleted successfully');
  });
});

 
 

2. Frontend 프론트엔드

이제 만들어놓은 API를 호출하여 수정과 삭제 기능을 완성해볼 것이다 
▷ React 컴포넌트에서 API를 호출하는 방법 진행 [ fetch 또는 axios 사용 ]

 
 

2-1. 게시글 수정 페이지 구현

수정 API 호출 함수(PUT 요청)

const editPost = async (postId, updatedData) => {
	try {
    	const response = await fetch('/api/posts/${id}', {
        	method: 'PUT',
            headers: {
            	'Conent-Type': 'application/json',
            },
            body: JSON.stringify(updatedData),
        });
 		
        if(response.ok) {
        	alert('Post updated successfully');
            //업데이트 후 게시글을 다시 가져오거나 UI를 업데이트한다 
        } else {
        	alert('Failed to update post');
        }
    } catch(error) {
    	console.error('Error updating post:', error);
    }
};

 
updatedData는 수정된 title과 content 객체로, 
예를 들어 {title: 'New Title', content: 'Updated content'} 형태가 된다 
 
 

2-2. 게시글 삭제 구현 

삭제 API 호출 함수(DELETE 요청)

const deletePost = async(postId) => {
	try {
    	const response = await fetch('/api/posts/${id}', {
       	method: 'DELETE', 
    	});
	
    if(response.ok) {
    	alert('Post deleted successfully');
        //삭제 후 게시글을 다시 가져오거나 UI를 업데이트한다
    } else {
    	alert('Failed to delete post');
   	 }
    } catch (error) {
    	console.error('Error deleting post:', error);
    }
};

 
▷PostContent.js에 추가해줘도 되고 별도의 API 관리 파일을 만들어 작성해도 된다 
 
 

2-3. 수정과 삭제 API 관리

여러 컴포넌트에서 editPost나 deletePost를 사용할 수 있으므로 api.js를 새로 만들어 관리하려 한다
> api.js 생성

api.js

// src/share/api.js
export const editPost - async (id, updatedData) => {
	try{
    	const response = await fetch('/api/posts/${id}'), {
        	method: 'PUT',
            headers: { 'Content-Type' : 'application/json' },
            body: JSON.stringify(updatedData),
        });
        
        if (response.ok) {
        	return { success: true };
        } else {
        	throw new Error('Failed to update post');
        }
    } catch (error) {
    	console.error('Error updating post:', error);
        return { success: false, error };
    }
}; 

export const deletePost = async (id) => {
	try {
    	const response = await fetch('/api/posts/${id}', {method: 'DELETE'});
        
        if (response.ok) {
        	return {success:true };
        } else {
        	throw new Erro('Failed to delete post');
        }
    } catch (error) {
    	console.error('Error deleting post:', error);
        return { success: false, error };
    }
};

 
 

2-4. 수정, 삭제 버튼에 이벤트 연결하기

 > PostContent.js
게시글을 표시하는 컴포넌트에 수정과 삭제 버튼을 추가하고 해당 버튼에 API 호출 함수를 연결해준다

button 추가

 
삭제는 PostContent.js에서 바로 이벤트 처리해주고 
수정은 PostEdit.js로 이동해 수정할 수 있도록 만들어주려고 한다 
 

내가 생각한 수정 프로세스
① PostContent.js에서 수정 버튼을 누르면 
② PostEdit.js 페이지로 이동해 제목과 내용을 수정할 수 있게 해준다
③ 수정이 완료된 후에는 PostList.js 페이지로 이동해 게시글 목록을 보여준다 

 
 

2-5. API 호출함수와 버튼을 추가

> PostContent.js 수정, PostEdit.js 생성
 
☞ api.js의 editPost, deletePost 불러오기

import { editPost, deletePost } from '../share/api'; //api.js에서 함수 불러오기

 
 
 

* 코드 수정 체크리스트 * 
☞ PostContent.js

▷기존에 axios.put과 axios.delete로 처리하던 handleEdit 함수와 handleDelete 함수를
editPost와 deletePost 함수로 호출하도록 변경 
 
▷editPost와 deletePost 함수는 이미 /api/posts/${id}로 요청을 보내므로 PostContent.js에서는 다시 해당 URL을 지정할 필요가 없으므로 제거
 
① PostContent.js에서 수정 버튼 클릭 시 PostEdit.js로 이동하는 부분 처리

//PostContent.js
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { deletePost } from '../share/api'; //api.js에서 함수 불러오기
import axios from 'axios';

const PostContent = () => {
	const { id } = useParams();
    const navigate = useNavigate(); //navigate 변수 선언
    const [post, setPost] = useState(null);
    
    useEffect(() => {
    	axios.get('http://localhost:5001/api/posts/${id}')
    		.then(response => setPost(response.data))
            .catch(error => console.error('게시글 불러오기 오류:', error));
    }, [id]);
    
    if(!post) return <div> 로딩 중...</div>;
    
    //수정함수 
    const handleEdit = () => {
    	//수정 페이지로 이동하는 함수
        navigate('/edit/${함수}');
    };
    
    // 삭제 함수
    /*
    // api.js 생성으로 인한 수정
    const handleDelete = () => {
    	axios.delete('http://localhost:5001/api/posts/${id}')
        	.then(() => {
            	alert('Post deleted successfully');
                navigate('/') //삭제 후 메인페이지로 이동
            })
            .catch(error => console.error('게시글 삭제 오류:', error));
    }; */
    
    const handleDelete = async () => {
    const result = await deletePost(id);
    
   		if(result.success) {
    		alert('Post deleted successfully');
            navigate('/'); //삭제 후 메인페이지로 이동
    	} else {
        	alert(result.error || 'Failed to delete post');
        }
    };
    
    
    return (
    	<div>
        	<h2>{post.title}</h2>
            <p>{post.content}</p>
            <p><strong>작성일</strong> {new Date(post.created_at).toLocaleString()}</p>
            {/* 버튼 추가 */}
            <button onClick={handleEdit}>Edit</button>
            <button onClick={handleDelete}>Delete</button>
        </div>
    );
};

export default PostContent;

 
 
② PostEdit.js에서 게시글을 수정가능하도록 구현 
> PostEdit.js 생성

수정 코드
PostEdit.js의 return

import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { editPost } from '../share/api';	//api.js에서 editPost 함수 불러오기
import axios from 'axios';

const PostEdit = () => {
	const { id } = useParams();	//URL에서 post ID를 가져옴
    const navigate = useNavigate();	//navigate 사용
    const [title, setTitle] = useState('');
    const [content, setContent] = useState('');
    
    useEffect(() => {
    // 게시글 데이터를 가져와서 수정할 수 있도록 제목과 내용 상태에 저장
    axios.get('http://localhost:5001/api/posts/${id}')
    	.then(response => {
        	setTitle(response.data.title);
        	setContent(response.data.content);
        })
        .catch(error => console.error('게시글 불러오기 오류:', error));
    },[id]);
    
    const handleSave = async () => {
    	const updatedData = { title, content };
        
        const result = await editPost(id, updatedData);
        
        if (result.success){
        	alert('Post updated successfully');
            navigate('/');	//수정 후 목록 페이지로 이동
        } else {
        	alert(result.error || 'Failed to update post');
        }
    };
    
    return (
    	<div>
        	<h2>Edit Post</h2>
            <div>
            	<label>Title</label>
                <input
                	type="text"
                    value={title}
                    onChange={(e) => setTitle(e.target.value)}	//제목 입력값 상태에 반영
                />
            </div>
            <div>
            <label>Content</label>
            <textarea 
            	value={content}
                onChange={(e) => setContent(e.target.value)}	//내용 입력값 상태에 반영
            />
            </div>
            <button onClick={handleSave}>Save</button>
            <button onClick={() => navigate('/')}>Cancel</button>
        </div>
    );
};

export default PostEdit;

 
 

③ Router 설정
> Router.js 파일 수정
PostEdit.js 페이지를 위한 라우트 태그를 추가한다 

Router.js

import React, { useState, useEffect } from 'react';
// 1. react-router-dom을 사용하기 위해서 BrowserRouter, Route, Routes를 import한다
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import PostList from '../components/PostLists';
import PostForm from '../components/PostForm';
import PostContent from '../components/PostContent';
import Posts from '../components/Posts';	//api/posts
import PostEdit from '../components/PostEdit';
import axios from 'axios';

// 2. Router라는 함수를 만들고 아래와 같이 작성
//BrowserRouter를 Router로 감싸는 이유는, 
//SPA의 장점인 브라우저가 깜빡이지 ㅇ낳고 다른 페이지로 이동할 수 있게 만들기 위해서!
const Router = () => {
	const [posts, setPosts] = useState([]);
    
    // 게시글 목록을 초기화하는 useEffect
    useEffect(() -> {
    axios.get('http://localhost:5001/api/posts')
    	.then((response) => setPosts(response.data))
        .catch((error) => console.error('Error fetching posts:', error));
    }, []);	//한 번만 실행
    
    return (
    	<BrowserRouter>
        	<Routes>
            	<Route path="/" element={<PostList />} />
                {/* posts와 setPosts를 전달 */}
                <Route path=:"/" element={<PostList posts={posts} setPosts={setPosts} />} />
                {/* setPosts 전달 */}
                <Route path="postForm" element={<PostForm setPosts={setPosts} />} />
                <Route path="/post/:id" element={<PostContent />} />
                {/* 게시글 수정 */}
                <Route path="/edit/:id" element={<PostEdit />} />
                {/* API 결과를 보여주는 페이지 */}
                <Route path="api/posts" element={<Posts />} />
            </Routes>
        </BrowserRouter>
    );
};

export default Router;

 
 
④  수정 후 PostList.js 목록 갱신
이미 구현되어 있음 

navigate('/')

 
⑤ Api.js 코드 참고

Api.js

 
 

3. UI 업데이트

수정이나 삭제 작업이 완료된 후 게시글 목록을 다시 불러오거나 로컬 상태를 업데이트하여 UI가 즉시 반영되도록 한다 
아래 두 가지 방법 중 선택 가능 
1) 상태 업데이트 : 예를 들어 삭제 후에는 setPosts 등의 상태 업데이트 함수를 사용해 현재 게시글 목록에서 삭제된 게시글을 제거한다 
2) 데이터 재요청 : 전체 게시글을 다시 불러오는 API를 호출해 최신 목록을 보여줄 수 있다 
 
 

4. 실행 및 테스트

4-1. 서버 실행

Express 서버 실행 → MySQL 연결

cd server
node server.js

 

Connected to MySQL

 
개발서버 실행

npm run start

 
 

4-2. 테스트

→ 글 작성 후 수정, 삭제까지 테스트
① 글 작성

PostForm

 

작성 완료

 
 
② 게시글 목록 확인

PostList

게시글 목록 자동 갱신
 
 
③ 글 수정

PostEdit

title과 content 수정 후 Save 버튼 클릭
→ Post updated successfully 창이 뜸
 
 
④ 수정 확인

수정 완료

 
⑤ 글 삭제

체크 표시한 글 삭제 예정

 
 
삭제하려는 글을 누르고 Delete 버튼 클릭

게시글 상세

 
Delete 버튼을 클릭하면 'Post deleted successfully' 창이 뜬다

alert

'수정하기'라는 제목의 글도 똑같이 삭제 진행해준다 
 
 
⑥ 삭제 확인
삭제된 후에는 자동으로 게시글 목록 이동 

글 2개가 삭제된 것을 확인해볼 수 있다 
 
게시판 CRUD 완료!

 

반응형