Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

9시 24분

게시판 CRUD 기능 만들기 본문

Javascript

게시판 CRUD 기능 만들기

leeeee.yeon 2021. 8. 1. 10:24

 

일단 가장 기본적인 GET, POST를 연습했으니 실전에 돌입해보자! (PUT, DELETE 등은 실전을 하면서 배우자)

 

 

글 생성

 

이제까지는 <textarea>를 이용하여 게시판 기능을 구현하였지만 이번에는 텍스트 에디터라는 것을 사용해보자!

WYSIWYG의 ckEditor을 한 번 사용해보자.

npm install --save @ckeditor/ckeditor5-react @ckeditor/ckeditor5-build-classic

 

설치가 완료되면 Create.js를 아래와 같이 바꾸자.

기존에 하던 방식과 비슷한데 <textarea> 자리에 CKEditor 컴포넌트가 들어갔다고 생각하면 된다.

import React from "react";
import "./Create.css";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";

function Create(){
    return (
        <div className="create__container">
            <h2>커뮤니티</h2>
            <p><input type="text" name="title" placeholder="title"/></p>
            <CKEditor
            editor={ClassicEditor}
            data='<p>Hello from CKEditor5!</p>'
            onReady={editor =>{

            }}
            onChange={(event, editor) => {
                const data = editor.getData();
                console.log({event, editor, data});
            }}
            onBlur={(event, editor) => {
                console.log('Blur', editor);
            }}
            onFocus={(event, editor) => {
                console.log('Focus', editor);
            }}
            />
            <p><input type="submit" value="글 작성" /></p>
        </div>
    );
}

export default Create;

 

그리고 입력과 관련된 기능들을 구현해주었다.

import React, { useState } from "react";
import "./Create.css";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import axios from "axios";

function Create(){
    const [content, setContent] = useState({
        title: '',
        description: '',
        date: ''
    });

    const submitContent = () => {
        axios.post('http://localhost:3002/content/create', {
            title: content.title,
            description: content.description,
            date: content.date
        })
        .then(()=>{
            console.log('client - send post data');
            alert('등록 완료!');
        })
        .catch((error)=>{ console.log(error); });
    }

    const getValue = e => {
        const {name, value} = e.target;
        let now = new Date();
        let timeString = now.toLocaleString();
        setContent({
            ...content,
            [name]: value,
            date: timeString
        });
        console.log(content);
    }

    return (
        <div className="create__container">
            <h2>커뮤니티</h2>
            <p><input type="text" name="title" placeholder="title" onChange={getValue}/></p>
            <div className="ckeditor">
                <CKEditor
                editor={ClassicEditor}
                data='<p>Hello from CKEditor5!</p>'
                onReady={editor =>{

                }}
                onChange={(event, editor) => {
                    const data = editor.getData();
                    // console.log({event, editor, data});
                    setContent({
                        ...content,
                        description: data
                    });
                    // console.log(content);
                }}
                onBlur={(event, editor) => {
                    // console.log('Blur', editor);
                }}
                onFocus={(event, editor) => {
                    // console.log('Focus', editor);
                }}
                />                
            </div>
            <p><input type="submit" value="글 작성" onClick={submitContent}/></p>
        </div>
    );
}

export default Create;

 

  • 게시판의 정보(제목, 글 내용, 날짜)가 담긴 content라는 state를 만들었다.
  • 이벤트가 발생하면 그 name과 value를 가져오는 함수 getValue()를 이용하여 content state를 업데이트해준다. (제목, 글 내용)
  • getValue()를 에디터 컴포넌트의 onChange 안에 넣어준다.
  • new Date()와 toLocaleString()을 이용하여 글을 작성한 시간도 state로 전달해주었다. (SQL, 헤로쿠에서 날짜 관련 함수를 쓸 때 잘 작동하지 않아 클라이언트 단에서 날짜 정보를 전달하는 방법을 사용해보기로 함)
  • axios.post로 서버에 컴포넌트 속 데이터를 전달해주었고, 이를 subimtContent()라는 함수로 만들어주었다.

 

이제 서버에서 클라이언트가 보낸 정보를 받아보자.

router.post('/create', (req, res) => {
    console.log('server - recieved data from client');
    console.log(req.body);
    res.send('ok');
});

클라이언트에게 정보를 잘 받은 것을 확인할 수 있다 !!

여기서 처음에 res.send를 해주지 않아 axios.post의 then 구문이 작동하지 않았다.

이를 통해 서버에서 정보(request)를 받으면 클라이언트에게 정보를 받았다는 대답(response)를 해줘야 클라이언트도 그 다음 작업을 수행한다는 것을 배웠다.

 

 

DB 연동

 

npm install --save mysql

mysql 모듈을 설치해주었다. 그리고 생활코딩에서 사용했던 방식처럼 db.js를 통해 db 관련 코드를 분리하였다.

나는 db.js를 public/javascripts 폴더에 넣었다.

 

const mysql = require('mysql');

const db = mysql.createConnection({
    connectionLimit: 20,
    host: 'localhost',
    user: 'root',
    password: '자신의 비밀번호',
    database: 'yeonmovie'
});

module.exports = db;

 

그 다음 database와 table을 만들어주었다. 참고로 내 database 이름은 yeonmovie, table 이름은 board이다.

-- DB 생성 --
create database web;


-- 글 관련 table 생성 --
create table create(
    id int(11) not null auto_increment,
    title varchar(100) not null,
    description text null,
    created varchar(100) not null,
    primary key(id)
);

 

나중에 회원 기능을 만들면 그와 관련된 column을 만들어야 해서 미리 author 관련 column도 만들까 생각했지만 일단 기본적인 기능만 만들고 나중에 column을 추가하는 방식으로 SQL을 수정하기로 했다.

(SQL 수정하는거 은근 귀찮아 ,,,)

 

서버에서 CRUD를 담당할 content.js라는 파일을 아래와 같이 만들었다. 

const express = require('express');
const router = express.Router();

const db = require('../public/javascripts/db');

router.get('/', (req, res)=>{
    res.status(200).json({
        test: "ok"
      });
});

router.post('/create', (req, res) => {
    console.log('server - recieved data from client');
    console.log(req.body);
    const title = req.body.title;
    const description = req.body.description;
    const date = req.body.date;
    const sqlQuery = 'INSERT INTO content (title, description, created) VALUES (?, ?, ?)';
    db.query(sqlQuery, [title, description, date], (err, result)=>{
        if(err) throw err;
        res.send('ok');
    });
});

module.exports = router;
  • router.get('/')은 localhost:3002/content에 들어갔을 때 잘 되는지 확인하는, 큰 의미가 없는 코드이다.

SQL 문법, request를 받아 SQL에 저장하는 코드는 앞에서 내가 생활코딩을 들으며 했던 내용과 깃허브를 참고하였으니, 자세한 설명은 생략한다.

블로그 정리: https://leeeeeyeon.tistory.com/78?category=1000387

깃허브: https://github.com/leeeeeyeon/template-node.js

혹시라도 참고할 사람은 위 링크로 고고 ...!

 

작성한 내용이 성공적으로 저장된 것을 볼 수 있다.

 

 

 

글 목록

 

DB에 저장된 정보를 가져와 작성한 글들의 목록을 띄워보자.

Board.js라는 파일을 만들어주자.

import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import "./Board.css";
import axios from "axios";

function Board(){

    const [viewContent, setViewContent] = useState([]);

    useEffect(()=>{
        axios.get('http://localhost:3002/board')
        .then((res) => {
            setViewContent(res.data);
        })
        .catch((err)=> { console.log(err); });
    }, []);

    return (
        <div className="board__container">
            <h2>커뮤니티</h2>
            <Link to="/board/create" className="create">create</Link>
            <table className="tbl">
                <thead>
                    <tr>
                    <th>No</th>
                    <th>제목</th>
                    <th>등록일</th>
                    </tr>
                </thead>
                <tbody>
                    {viewContent.map(element =>
                        <tr>
                            <td>{element.id}</td>
                            <td>{element.title}</td>
                            <td>{element.created}</td>
                        </tr>
                    )}
                </tbody>

            </table>
        </div>
    );
}

export default Board;
  • 글들의 목록을 저장할 viewContent라는 state을 만들고, default로 빈 배열을 주었다.
  • useEffect 훅을 사용하고, axios로 서버에 요청을 보내 데이터를 받아온다.
  • 두 번째 인자로 빈 배열을 넣어야 코드가 처음에만 실행된다. (그렇지 않으면 서버 터미널에서 집착광공 같은 GET 메시지를 보게 될 것이다 ... ^ㅅ^)
  • 받아온 데이터를 setViewContent()를 통해 viewContent state에 넣어준다.
  • map을 이용하여 최종적으로 viewContent 배열에 담긴 글들을 목록으로 보여주었다.

여기서 주의할 것 !!! axios의 then 구문에서 res 대신 response로 하면 에러가 난다 !!

이것은 내가 서버 단에서 응답을 뜻하는 변수를 res라고 하였기 때문이다. 그렇기 때문에 클라이언트에서도 res를 변수로 사용해주어야 한다.

 

결과물

 

 

 

상세 글

 

글 목록에서 제목을 클릭하면 해당 글 페이지로 가도록 해보자.

처음에는 글 목록에서 했듯이 axios.get으로 서버에게 정보를 받아오는 방법을 생각했는데, URL에서 계속 막혔다.

그러던 도중 Movie.js랑 Detail.js의 코드를 보고 해답을 찾을 수 있었다 !!! 우리는 이미 노마드 코더 강의에서 이에 대한 내용을 다룬 적이 있는 것이었다!

 

우리가 원하는 것은 제목을 클릭했을 때 해당 게시글의 내용을 읽고 싶은 것이다.

Board.js에서 제목에 해당하는 부분을 Link 태그로 감싸주자.

<td><Link to={{
  pathname: `/board/${element.id}`,
  state: {
  title: element.title,
  description: element.description,
  date: element.created
  }
}}>{element.title}</Link></td>

우리가 이동할 페이지가 될 pathname과 함께 state라는 객체를 전달해주자. 받은 state를 통해 상세 글을 보여주면 되는 것이다.

 

이제 게시글의 내용을 표시할 Read.js라는 파일을 생성하자.

본격적으로 Read.js의 내용을 적기 전에 게시글에 접근할 수 있도록 App.js에서 게시글을 담당할 라우터도 만들어주자.

<Route path="/board-create" component={Create}/>
<Route path="/board/:boardId" component={Read}/>

근데 이 때 !!! 'board/create'과 'board/:boardId'가 둘 다 board의 하위 계층이여서 'board/create'도 Read.js 파일을 읽어오게 됐다. exact={true}를 사용해보았지만 잘 되지 않아 그냥 create의 URL을 바꿔주는 방법을 택했다 ^_ㅠ

( 귀찮은거 시렁 ,,, )

 

아래는 Read.js의 코드이다.

import React, { useEffect } from "react";
import ReactHtmlParser from 'react-html-parser';
import "./Read.css";

function Read(props){
    const { location, history } = props;
    useEffect(()=>{
        if(location.state === undefined){
            history.push("/board");
        }
    }, [location, history]);

    return (
        <div className="read__container">
            <ul>
                <li>제목: {location.state.title}</li>
                <li>작성 날짜: {location.state.date}</li>
                <li>{ReactHtmlParser(location.state.description)}</li>
            </ul>
        </div>
    );
}

export default Read;

props에는 다음과 같은 정보가 들어있다. 코드에서 볼 수 있듯이 우리는 여기서 history, location을 사용해줄 것이다.

history는 리다이렉트에, location은 Board.js에서 전달한 데이터를 가져오는데 사용할 것이다.

 

근데 !!! 코드를 작성하면서 처음에 계속 props가 전달이 안됐다 ㅡㅡ

알고보니 App.js에서 경로 맨 앞에 "/board/:boardId"에서 '/'을 빼먹어 "board/:boardId"라고 적은 것이었다 ^ㅅ^;;;

슬래시를 빼먹지 말자 ㅋㅅㅋ ,,, ~~~

 

useEffect 훅 안에서 state가 존재하지 않을 때 ( ex. /board/8이라는 게시글이 없는데 그에 접근하는 경우 )

글 목록 페이지로 리다이렉트해주었다.

그리고 원래는 최초 1회만 렌더링 되도록 두 번째 인자로 빈 배열을 주는데

이런 경고가 뜨길래 배열에 location과 history를 주어서 해결했다.

 

아 ! 그리고 ckEditor5에 의해 본문을 작성할 때 HTML 태그가 포함된 채 저장된다.

react-html-parser 모듈을 설치해주어서 우리가 작성한 내용만 나오도록 하는 것도 해주었다.

 

짜잔 결과물이다 (o゜▽゜)o☆

 

 

 

글 수정

 

이번에는 게시글을 수정해보자. 글 수정 관련 프론트엔드 로직은 Create.js와 거의 비슷하다.

우선 UI를 만들자.

  • Read.js에 글 수정 페이지로 가는 버튼을 만들고,
  • 글 수정 페이지의 UI를 만든다.

우리는 Board.js에서 Read.js로 state을 전달해서 게시글을 읽었다.

이번에는 Read.js에서 Update.js로 state을 전달해서 수정 전 글 내용을 불러오자.

 

아래는 Read.js의 글 수정 버튼과 관련된 코드이다.

<Link to={{
  pathname: `/board/update/${location.state.id}`,
  state: {
  id: location.state.id,
  title: location.state.title,
  description: location.state.description
}
}} className="update">update</Link>

 

그리고 이쯤에서 HTTP 코드 복습복습

  • GET: 데이터 조회
  • POST: 데이터 등록 및 전송
  • PUT: 데이터 수정
  • DELETE: 데이터 삭제

생활코딩에서는 글 수정과 삭제도 post를 사용했는데 이번에는 HTTP 코드에 맞게 PUT, DELETE를 사용해보자.

이번에 사용할 것은 PUT이다.

put 메서드는 서버 내부적으로 get > post 과정을 거치기 때문에 post 메서드와 비슷한 형태이다.

 

아래 코드는 Update.js의 완성본이다.

import React, { useEffect, useState } from "react";
import { CKEditor } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import "./Update.css";
import axios from "axios";

function Update(props){
    const { location, history } = props;

    const [content, setContent] = useState({
        title: location.state.title,
        description: ''
    });

    const getValue = e => {
        const {name, value} = e.target;
        setContent({
            ...content,
            [name]: value,
        });
    }

    const updateContent = () => {
        axios.put('http://localhost:3002/board/update', {
            id: location.state.id,
            title: content.title,
            description: content.description
        })
        .then(()=>{
            alert('수정 완료!');
            history.push(`/board`);

        })
        .catch((err)=>{ console.log(err); });
    }

    useEffect(()=>{
        if(location.state === undefined){
            history.push("/board");
        }
    }, [location, history]);

    return (
        <div className="update__container">
            <h2>커뮤니티</h2>
            <p><input type="text" name="title" placeholder="title"
                value={content.title}
                onChange={getValue}/></p>
            <div className="ckeditor">
                <CKEditor
                editor={ClassicEditor}
                data={location.state.description}
                onReady={editor =>{

                }}
                onChange={(event, editor) => {
                    const data = editor.getData();
                    // console.log({event, editor, data});
                    setContent({
                        ...content,
                        description: data
                    });
                }}
                onBlur={(event, editor) => {
                    // console.log('Blur', editor);
                }}
                onFocus={(event, editor) => {
                    // console.log('Focus', editor);
                }}
                />                
            </div>
            <p><input type="submit" value="글 수정" onClick={updateContent}/></p>
        </div>
    );
}

export default Update;
  • 처음에 제목 input 태그의 value 속성을 {location.state.title}을 사용하니 입력이 동작하지 않았다. 그래서 useState을 통하여 content state에 초기값을 주고, value 속성에 content.title을 주었다.
  • updateContent() 함수를 보면 '/board'로 리다이렉트했는데, 처음에는 게시글로 리다이렉트하기 위해 '/board/${id}'라고 하니 아래 사진과 같은 에러가 발생하였다. 그래서 그냥 간단하게 board로 리다이렉트하도록 만들었다.

axios.get을 해서 id를 받아오거나 다른 방법을 사용할 수도 있지만, 번거롭다! 공부를 위해 만드는 것이니 번거로운 것들은 굳이 일일이 하지 말자.

리다이렉트 에러

 

 

글 삭제

 

드디어 CRUD의 마지막인 글 삭제 ...!

 

delete 메서드는 일반적으로 body가 비어있다. (서버로 데이터를 전송하지 못함)

그래서 get과 비슷한 형태를 띄지만, delete 메서드가 서버에 들어가게 되면 삭제가 진행된다.

 

하지만 두 번째 인자에 data: {}라는 attribute을 주면 서버에 데이터를 전송할 수 있다.

 

일단 delete UI를 만들자.

<input type="submit" value="delete"
className="delete" onClick={deleteContent}/>
.update, .delete{
    background: #E8F6EF;
    border-radius: 5px;
    color: black;
    margin: 1px;
    margin-right: 10px;
    font-weight: 500;
    border: 0;
    outline: 0;
  }

.delete:hover{
  background-color: #F6AE99;
}

 

글을 삭제하는 process는 렌더링될 페이지가 필요없기 때문에 Delete.js를 만들지 않고, Read.js에서 그 과정을 진행하였다. 그렇기 때문에 Link 태그 대신 input 태그를 사용해주었다.

CSS 면에서는, margin-right을 통해 update, delete 버튼 간에 간격 만들고, border과 outline을 0으로 해주어서 테두리를 지우고, 마우스가 올라갈 시 색이 변하도록 해주었다.

 

onClick 속성에 deleteContent() 함수가 들어가있는데, deleteContent() 함수는 아래와 같다.

const deleteContent = () => {
  axios.delete(`http://localhost:3002/board/delete`, {
    data: {
    id: location.state.id
    }
  })
  .then(()=>{
  alert('삭제 완료!');
  history.push('/board');
  })
  .catch((err)=>{ console.log(err); });
}
  • 서버에서 삭제할 게시글을 구분할 수 있도록 id를 전송하였다. ( 이것이 위해서 말한 2번째 인자에 data attriubte을 추가해준 것)
  • 삭제가 완료되면 게시글 목록 페이지로 리다이렉트한다.

 

서버 단의 삭제 process는 아래와 같다.

router.delete('/delete', (req, res)=>{
    const id = req.body.id;
    const sqlQuery = 'DELETE FROM board WHERE id=?';

    db.query(sqlQuery, [id], (err, result)=>{
        if(err) throw err;
        res.send('ok');
    });
});

삭제가 잘 되는 모습

 

이로써 기본적인 CRUD를 모두 만들었다 ^ㅅ^ !

백엔드의 큰 틀은 생활코딩에서 했던 것을 응용하였기 때문에 큰 어려움이 없었지만,

프론트엔드에서 여러 시행착오와 어려움을 겪었던거 같다.

 

하지만, 그 과정 속에서 그동안 계속 궁금해 했던 데이터 통신에 대해 명확히 이해할 수 있었다.

또, 이전에는 get과 post 메서드만 사용했는데 상황에 맞게 get, post, put, delete 메서드를 사용하는 법을 배웠고, 이와 함께 204, 404 등등 여러가지 HTTP 상태 코드를 접할 수 있었다 : )


참고

 

 

'Javascript' 카테고리의 다른 글

Typescript로 블록체인 만들기  (0) 2021.08.23
게시판 기능 리팩토링  (0) 2021.08.04
React + Node.js 연동  (0) 2021.07.29