Reactjs, Nodejs로 최대한 많은 내용을 복습하며 게시판 만들어보기 - 백엔드 코드 리뷰

Design.C·2021년 12월 22일
1

React-Nodejs 복습

목록 보기
2/4
post-thumbnail

백엔드에서 복습해본 키워드

https://github.com/znehraks/bulletinBoard-backend

디렉토리 구조

index.js

//index.js
//.env환경변수를 사용하기 위한 라이브러리
require("dotenv").config();
const express = require("express");
const app = express();

//CORS 에러를 해결하기위한 라이브러리
const cors = require("cors");

//게시판 데이터 교환을 담당하는 라우터
const postRouter = require("./router/post");

//회원정보 데이터 교환을 담당하는 라우터
const userRouter = require("./router/user");

//로그를 기록하는 라이브러리
const morgan = require("morgan");

const path = require("path");

//주기별로 파일을 기록하는 라이브러리
const rfs = require("rotating-file-stream");

//CORS의 옵션 설정 객체: 클라이언트 "https://bulletinboard-designc.netlify.app"에서 들어온 요청만 응답해주겠다는 설정
const corsOptions = {
  origin: "https://bulletinboard-designc.netlify.app",
  // origin: "http://localhost:3000",
  credentials: true,
};
//위에서 작성한 옵션을 cors함수의 인자로 전달하고 그것을 app에서 사용한다.
app.use(cors(corsOptions));

//하루 간격으로 combined레벨의 접근로그를 access.log라는 파일명으로 기록하여 저장하는 코드
// const accessLogStream = rfs.createStream("access.log", {
//   interval: "1d",
//   path: path.join(__dirname, "log"),
// });
// app.use(morgan("combined", { stream: accessLogStream }));

//'/user' url로 들어온 요청에 대해 처리하는 라우터
app.use("/user", userRouter);

//'/post' url로 들어온 요청에 대해 처리하는 라우터
app.use("/post", postRouter);

//환경변수에 설정된 PORT에서 서버를 돌아가게한다.
app.listen(process.env.PORT, () => {
  console.log(`listening on port ${process.env.PORT}`);
  console.log(process.env.PORT);
});

라우팅

'라우팅이란 경로를 설정해주는 것'이라는 네트워크 용어이다.
클라이언트에서 회원정보에 해당하는 user데이터를 원한다면 /user와 같은 url을 통해 요청하게 되고,
서버에서 /user로 들어온 요청(request)을 처리하는 로직을 작성함으로써 클라이언트가 원하는 응답(response)을 하게 된다.

post라우터

/router/post.js

///router/post.js

//express의 라우터를 사용하기 위해 로드한다.
const express = require("express");
const router = express.Router();

//post 요청을 통해 들어온 req.body의 파라미터를 파싱하는 라이브러리
const bodyParser = require("body-parser");

//클라이언트에서 들어온 요청을 json형식으로 변환(파싱)한다.
const parser = bodyParser.json();

//데이터베이스와의 연결을 설정하여 결과를 반환하는 코드(아래에서 다룬다)
const connection = require("../connection");

//메인페이지 게시판 모두 불러오기
//url이 '/post/'이며, get method로 들어온 요청에 대해 처리하는 라우팅 함수
router.get("/", (req, res) => {
  //실행할 sql작성
  //board 테이블의 모든 열(row) 중에 삭제되지 않은 항목(is_deleted=0)을 생성날짜(created_at)역순으로 선택해오는 sql문이다.
  const sql = "SELECT * FROM board WHERE is_deleted=0 ORDER BY created_at DESC";
  //위에 작성한 sql문을 인자로 받고 뒤의 화살표함수를 실행하는 문
  //err은 connection.query함수에서 에러가 발생했을때 실행되는 에러
  //data는 connection.query함수가 성공적으로 실행되었을때 sql문을 통해 가져오는 데이터가 json의 배열 형태로 담겨짐
  connection.query(sql, (err, data, fields) => {
    //에러가 있다면 에러를 발생
    if (err) throw err;
    //에러가 없다면 data를 클라이언트로 전송한다.
    res.send(data);
  });
});

//게시판 글 작성하기
//'/post/create'라는 url로 post method로 요청이 들어왔을 때 처리하는 라우터
//parser를 미들웨어로 넣어줘야 req.body.인자를 성공적으로 불러올 수 있다.
router.post("/create", parser, (req, res) => {
  //게시판 작성자의 회원코드, 회원아이디, 게시판제목, 게시판내용을 받아 데이터베이스에 삽입하는 sql문
  const sql = `INSERT INTO board(user_code, board_author, board_title, board_content) VALUES(${req.body.user_code},'${req.body.board_author}','${req.body.board_title}','${req.body.board_content}')`;
  connection.query(sql, (err, data, fields) => {
    if (err) throw err;
    res.send(data);
  });
});

//게시판 글 수정하기
//'/post/update/게시판고유코드'를 url로 받아 게시판의 내용 수정 기능을 처리하는 라우터
router.put("/update/:code", parser, (req, res) => {
  //게시판의 고유코드가 url로 넘어온 파라미터속 :code와 일치하는 row에 대해 게시판제목(board_title)과 게시판내용(board_content) column을 변경시키는 sql문
  const sql = `UPDATE board SET board_title = '${req.body.board_title}', board_content='${req.body.board_content}' where board_code = '${req.params.code}'`;
  connection.query(sql, (err, data, fields) => {
    if (err) throw err;
    res.send(data);
  });
});

//게시판 글 삭제하기(삭제처럼 보이게 하기)
//'/post/delete' url로 들어온 요청을 받아 삭제처리를 담당하는 라우터
router.post("/delete", parser, (req, res) => {
  //body에 들어온 파라미터중 게시판고유코드와 일치하는 row의 is_deleted 컬럼을 1로 변경시켜 삭제처리(클라이언트에서 이용하도록)해주는 sql문
  const sql = `UPDATE board SET is_deleted = 1 where board_code=${req.body.board_code}`;
  connection.query(sql, (err, data, fields) => {
    if (err) throw err;
    res.send(data);
  });
});

//라우터 모듈을 다른 파일에서 쓸 수 있도록 내보내는 문
module.exports = router;

user라우터

/router/user.js

const express = require("express");
const router = express.Router();
const bodyParser = require("body-parser");

//jsonwebtoken 싸인 및 검증을 위해 자주 쓰이는 함수
const authenticateJWT = require("../utils");
const parser = bodyParser.json();
const connection = require("../connection");

//비밀번호 암호화를 위한 라이브러리
const bcrypt = require("bcrypt");

//jwt 라이브러리
const jwt = require("jsonwebtoken");

//특정 유저 정보 불러오기
//현재 로그인된 유저의 프로필페이지에서 보여질 데이터를 응답하는 라우터
//'/user'의 url에 get method로 들어온 요청에 대해 처리함
router.get("/", (req, res) => {
  try {
    //es6의 전개연산자를 통해 클라이언트에서 header에 첨부한 authorization(token)인자를 이용하여 jwt 토큰 인증에 성공해서 얻은 내부 데이터 중 code변수를 불러온다.
    const { code } = authenticateJWT(req.headers.authorization);
    //위에서 불러온 code변수를 조건으로 하여 board테이블과 user테이블을 user테이블에 대해 left join 한 결과를 불러온다.
    const sql = `SELECT * FROM user LEFT JOIN board ON user.user_code = board.user_code WHERE user.user_code = ${code};`;
    connection.query(sql, (err, data, fields) => {
      if (err) throw err;
      res.send(data);
    });
    //위의 과정 중 에러가 발생하면 success: false라는 json 응답을 전송한다.
  } catch (e) {
    res.send({ success: false });
  }
});

//회원가입하기
router.post("/create", parser, async (req, res) => {
  console.log(req.body);
  //body에 post로 들어온 user_password 항목을 bcrypt를 통해 암호화하여 hashedPassword에 할당.
  //암호화된 비밀번호와 함께 다른 항목을 데이터베이스에 삽입한다.
  const hashedPassword = await bcrypt.hash(req.body.user_password, 10);
  const sql = `INSERT INTO user(user_name, user_id, user_password) VALUES('${req.body.user_name}','${req.body.user_id}','${hashedPassword}')`;
  connection.query(sql, (err, data, fields) => {
    if (err) throw err;
    res.send(data);
  });
});

//로그인하기
router.post("/login", parser, async (req, res) => {
  //먼저 body로 들어온 user_id와 일치하는 user_id를 가진 row를 가져오는 sql문
  const sql = `SELECT * FROM user WHERE user_id = '${req.body.user_id}'`;
  connection.query(sql, (err, data) => {
    if (err) {
      console.log(err);
      //쿼리문 실행 시 에러가 발생하면 success:false, 에러코드:-1, 에러메시지를 전송한다.
      res.send({
        success: false,
        err_code: -1,
        err_msg: "예기치 못한 오류가 발생했습니다.",
      });
    }
    //들어온 아이디에 해당하는 회원이 존재하지 않을 때
    if (data.length === 0) {
      res.send({
        success: false,
        err_code: 1,
        err_msg: "가입되지 않은 아이디입니다.",
      });
      return;
    }
    //들어온 아이딩에 해당하는 회원을 찾았을 경우
    //bcrypt를 통해 찾은 회원의 비밀번호와 데이터베이스에 저장(hashed)된 비밀번호를 비교하여 일치 여부를 확인한다.
    bcrypt.compare(
      req.body.user_password,
      data[0].user_password,
      (err, compareRes) => {
        //bcrypt 라이브러리 실행 도중 자체 에러 발생 시
        if (err) {
          console.log(err);
          res.send({
            success: false,
            err_code: -1,
            err_msg: "예기치 못한 오류가 발생했습니다.",
          });
        }
        //비밀번호가 일치할 경우
        if (compareRes) {
          //jwt를 통해 객체에 회원코드, 회원아이디를 추가하고 비밀키로 싸인한 뒤, 토큰으로 저장하고 이를 클라이언트에 success:true 필드와 함께 전송한다.
          const token = jwt.sign(
            { code: data[0].user_code, user_id: data[0].user_id },
            process.env.SECRET_KEY
          );
          res.send({ success: true, token });
        //비밀번호가 일치하지 않을 경우
        } else {
          res.send({
            success: false,
            err_code: 2,
            err_msg: "비밀번호가 틀립니다.",
          });
        }
      }
    );
  });
});

//url '/user/me'로 get method를 통해 들어온 요청을 처리하는 라우터
//현재 로그인된 회원이 누구인지 jwt 토큰 확인을 통해 인증(authentication)이 성공하면, 해당 회원의 데이터를 전송한다.
router.get("/me", (req, res) => {
  try {
    //클라이언트에서 보낸 jwt 토큰을 인증처리하여 결과를 user에 담고 클라이언트에 전송함
    const user = authenticateJWT(req.headers.authorization);
    res.send(user);
    //위 과정 중 에러가 발생했을 때
  } catch (err) {
    console.log(err);
    res.send({ success: false });
  }
});

module.exports = router;

데이터베이스 연동

mysql라이브러리를 사용할 수 있는 mariadb를 이용하였다. 개발 단계에서는 로컬환경에 구축하여 이용하였고, 배포 시에 awsRDS의 mariadb 프리티어를 구축하였다.

여기서 생성된 con은 위에 작성된 라우팅 코드의 쿼리문을 실행하기 위한 connection객체로 활용된다.

awsRDS구축

//connection.js
//mysql(mariadb)데이터베이스와 연동하기 위해 mysql 라이브러리 로드
const mysql = require("mysql");

//환경변수에 저장해놓은 호스트, 유저, 유저비밀번호, 데이터베이스명을 불러와 연결을 설정한다.
const con = mysql.createConnection({
  host: process.env.HOST,
  user: process.env.USER,
  password: process.env.PASSWORD,
  database: process.env.DATABASE,
});

//연결된 con을 내보낸다.
module.exports = con;

jwt인증방식

로그인 및 회원식별 구현기능 중 안전하다고 판단되는 인증 기술인 jsonwebtoken(jwt)방식을 이용했다.

jwt 인증방식을 내가 이해한대로 설명하자면, 예를들어, 이런 상황에 대한 가정이 가능하다.

어떤 가수의 콘서트장에 들어가려면 해당 가수의 싸인이 쓰여진 정품 티켓만 입장이 가능하다.
티켓에 숨겨진 예약 번호는 위의 조건이 충족될 시에 콘서트장에서만 직원에 의해 확인 가능한 상태이다.
또한, 이 정품 티켓을 구매하기 위해선 콘서트장에서 자신의 인적사항을 기재해야 구매할 수 있다고 가정한다.

  • 해당 가수의 싸인은 jwt 함수를 실행할 때 쓰이는 SECRET_KEY로 비유할 수 있다.
  • 티켓에 숨겨진 예약 번호는 jwt에서 SECRET_KEY와 함께 전달하는 code, user_id과 담긴 user 객체로 비유할 수 있다.
  • 정품 티켓을 구매하기 위해 자신의 인적사항을 기재하는 행위는, 초기에 jwt 토큰을 발급하기 위해 로그인을 하는 행위로 비유할 수 있다.

utils.js

//utils.js
const jwt = require("jsonwebtoken");
//jwt 인증함수
const authenticateJWT = (authorization) => {
  //함수를 호출하는 측에서 헤더에 속한 authorization(토큰)을 인자로 받아서 앞부분인 bearer 공백으로 분리해서 뒷부분인 순수한 토큰만을 인증에 사용한다.
  //인증에 성공하면 {code, user_id}가 담긴 user객체를 반환한다.
  const user = jwt.verify(authorization.split(" ")[1], process.env.SECRET_KEY);
  return user;
};
module.exports = authenticateJWT;

환경변수

버전관리를 위해 git에 업로드할때 아래에 기재된 사항과 같은 민감한 정보를 그대로 올리는 것은 위험하다. 따라서 .gitignore 파일에 .env를 등록해주며, 그러한 변수들은 환경변수로 관리해야 한다.

.env

PORT='서버를 켜둘 포트번호'
HOST='접속할 데이터베이스의 호스트 url'
USER='데이터베이스 접속 user명'
PASSWORD='데이터베이스 접속 USER의 비밀번호'
DATABASE='접속할 데이터베이스 이름'
SECRET_KEY='JWT토큰 생성 및 인증에 사용할 비밀키'

CORS

CORS는 간단히 말하면, 클라이언트 측이 자신과 출처가 다른 서버에 리소스를 요청할 때에 일어나는 에러이다.

1. 클라이언트에서 요청을 보내게 되면 브라우저에서는 Origin 필드에 자신의 출처를 보낸다.
2. 서버에서는 이렇게 들어온 요청에 대해 응답할 때 Access-Control-Allow-Origin 필드에 자신의 리소스에 접근이 허용된 출처에게만 응답을 하게 된다.
3. 만약 서버 응답의 Access-Control-Allow-Origin에 포함되지 않는 출처로부터 요청이 들어왔다면, CORS에러를 발생시킨다.

index.js

//index.js의 일부
const corsOptions = {
  //이 서버의 경우, Access-Control-Allow-Origin에 해당하는 출처로 배포된 클라이언트 출처인 https://bulletinboard-designc.netlify.app 만을 허용했다.
  origin: "https://bulletinboard-designc.netlify.app",
  // origin: "http://localhost:3000",
  credentials: true,
};
app.use(cors(corsOptions));

로그기록

간단하게 로그를 기록할 수 있는 기능을 구현했다.

morgan 라이브러리를 이용하여 로그 기록 수준은 'combined'("표준 Apache combined")레벨로 출력하도록 했다.

index.js

//index.js
//log 디렉토리 내에 access.log 파일에 하루('1d')간격으로 기록하도록 하는 코드
const accessLogStream = rfs.createStream("access.log", {
  interval: "1d",
  path: path.join(__dirname, "log"),
});
app.use(morgan("combined", { stream: accessLogStream }));
profile
코더가 아닌 프로그래머를 지향하는 개발자

0개의 댓글