본문으로 바로가기

환경변수 파일 생성


환경변수는 전역변수 보다 한 단계 위에 있는 변수라고 생각하면 쉬운데

프로그램이 실행되는 환경(시스템)에 저장되어 있는 변수로 해당 환경에 있는 모든 프로그램이 사용할 수 있다

웹 프로그래밍에서 쓰이는 환경 변수는 환경별로 달라질 수 있는 값이나, 보안 상의 이유로 코드에서 숨겨야할 때 환경 변수에 설정해놓고 쓰게된다

dotenv는 이런 환경 변수들을 파일에 넣고 사용할 수 있게 하는 라이브러리인데 .env로 만들고 그 안에 내용을 집어 넣으면 된다

예제 코드를 보기에 앞서 esm 라이브러리를 이용해서 모든 exports와 require는 export 와 import로 대체했다

require('dotenv').config();
import Koa from 'koa';
import Router from 'koa-router';
import bodyParser from 'koa-bodyparser';
import mongoose from 'mongoose';
import api from "./api";

const {PORT, MONGO_URI} = process.env;

mongoose
    .connect(MONGO_URI, {useNewUrlParser : true, useFindAndModify: false})
    .then(()=>{
        console.log(`Connected to MongoDB`);
    }).catch(e=>{
    console.error(e);
});

const app = new Koa();
const router = new Router();

router.use('/api',api.routes());

app.use(bodyParser());
app.use(router.routes()).use(router.allowedMethods());

const port = PORT || 4000;
app.listen(port,()=>{
    console.log(`Listening to port ${port}`);
});

환경 변수 파일은 require('dotenv').config() 함수로 불러올 수 있으며 불러온 뒤에는 따로 변수에 담거나 하지 않아도

process.env로 접근이 가능하다

데이터베이스의 스키마와 모델


  • 스키마 : 컬렉션에 들어가는 문서 내부의 각 필드가 어떤 형식으로 되어 있는지 정의하는 객체
  • 모델 : 스키마를 이용해서 만드는 인스턴스 객체

스키마 생성


모델을 만들기위해선 스키마가 필요하고 블로그 포스트에 필요한 스키마를 준비해야한다

  • 제목
  • 내용
  • 태그
  • 작성일
import mongoose from 'mongoose';

const {Schema} = mongoose;

const PostSchema = new Schema({
    title : String,
    body : String,
    tags : [String],
    publishedDate :{
        type : Date,
        default : Date.now,
    },
});

const Post = mongoose.model('Post',PostSchema);

export default Post;

몽구스 라이브에 PostSchema 스키마를 만든 뒤에 모델 인스턴스를 만들어서 내보냈다

데이터 생성과 조회


데이터 생성


모델을 만들어 줄 스키마를 만들었으니 이 스키마를 사용해서 진짜 모델을 만들어 주면된다

import Post from "../../models/post";

let postId = 1;

const posts = [
    {
        id: 1,
        title : 'title',
        body : 'contents',
    },
];

export const write = async ctx =>{
    const {title, body, tags} = ctx.request.body;

    const post  = new Post({
        title,
        body,
        tags,
    });
    try{
        await post.save();
        ctx.body = post;
    } catch (e){
        ctx.throw(500,e);
    }
};
...

ctx.request.body에서 받아온 데이터들로 Post의 인스턴스를 만들어 준다

이 인스턴스 내부에는 save라는 함수가 있는데 이 함수를 호출해 주어야 실제로 DB에 저장된다

반환값이 Promise이므로 async/await, try/catch 를 이용해주자

데이터 조회


모델을 만들어서 저장되는 것을 확인 했으니 이제 저장한 데이터를 조회하는 기능을 구현할 차례다

export const list = async ctx=>{
    try{
        const posts = await Post.find().exec();
        ctx.body = posts;
    } catch (e){
        ctx.throw(500,e);
    }
};

역시나 Post 인스턴스의 내부 함수로 불러오는데 find().exec(); 를 호출하면 쿼리를 만든 뒤에 서버에 날린다

exec()가 날리는 역할을 하는 걸로 봐선 find()가 쿼리 생성인 것 같다

마찬가지로 async/await, try/catch를 활용해주고 받아온 posts를 body에 넣어주면 된다

특정 데이터 조회


export const read = async ctx=>{
    const {id} = ctx.params;
    try{
        const post = await Post.findById(id).exec();
        if(!post){
            ctx.status = 404;
            return;
        }
        ctx.body = post;
    }catch (e){
        ctx.throw(500,e);
    }
};

딱히 특별할게 없다

데이터 삭제와 수정


데이터 삭제


export const remove = async ctx=>{
    const {id} = ctx.params;
    try{
        await Post.findByIdAndRemove(id).exec();
        ctx.status = 204;
    }catch (e){
        ctx.throw(500,e);
    }
};

위에서 한 것과 비슷하다 대신 받아올 객체가 없으므로 그냥 쿼리를 날리고 끝

데이터 수정


export const update = async ctx => {
    const {id} = ctx.params;
    try{
        const post = Post.findByIdAndUpdate(id,ctx.request.body, {
            new: true,
        }).exec();
        if(!post){
            ctx.status = 404;
            return;
        }
        ctx.body = post;
    }catch (e){
        ctx.throw(500,e);
    }
};

수정할 id값, 내용을 넣고 쿼리를 날리면 바로 디비에 있는 데이터가 수정이 되는데

세 번째 파라미터로 오는 객체는 업데이트의 옵션이다

요청 검증


ObjectId 검증


앞에서 구현한 read, remove, update는 모두 Id값이 필요하다

현재는 id값에 어떤 값이 들어가도 다 같은 500 에러가 나오고 있다 id값이 아닌 전혀 다른 문자열이 들어가면 bad request인 400에러를 출력해야하는데

이를 위해서 요청이 들어오는 id값을 검증해야한다

검증 방법은 다음과 같다

const {ObjectId} = mongoose.Types;
ObjectId.isValid(id);

매우 간단해서 remove, update, read api구현단에 넣어주기만 하면 되는데

중복되는 코드가 많아져서 dry하게 코딩하려면 미들웨어로 만들어주어야한다

const {ObjectId} = mongoose.Types;

export const checkObjectId = (ctx,next)=>{    
    const {id} = ctx.params;
    if(!ObjectId.isValid(id)) {
        ctx.status = 400;
        return ;
    }
    return next();
};

ctx.params에서 읽어 온 id값이 유효한지 검사하고 그렇지 않으면 400에러를 내고 바로 종료

유효하다면 next()를 호출해서 다음 미들웨어를 시작

posts라우터들을 관리하는 파일에서 미들웨어를 추가시켜주면된다

import Router from 'koa-router';
import * as postsCtrl from './post.ctrl';
import {checkObjectId} from "./post.ctrl";

const posts = new Router();

posts.get('/',postsCtrl.list);
posts.post('/',postsCtrl.write);
posts.get('/:id',checkObjectId, postsCtrl.read);
posts.delete('/:id',checkObjectId, postsCtrl.remove);
posts.patch('/:id',checkObjectId, postsCtrl.update);

export default posts;

Request Body 검증


Id와 마찬가지로 이번엔 write와 update에서 전달 받은 내용을 검증해야한다

포스트를 작성할 때는 title, body, tags 하나도 빠짐없이 작성해주어야하는데 현재는 3개 모두 빈칸으로 날려도 빈 내용의 포스트가 성공적으로 등록된다

각각의 내용을 검증하기 위해서 if문을 써서 비교해도 되지만 Joi 라이브러리를 사용해보도록하자

export const write = async ctx =>{

    const schema = Joi.object().keys({
                // 객체가 필드를 다 가지고 있음을 검증하고
        title : Joi.string().required(),// required()는 반드시 작성해야하는 항목
        body : Joi.string().required(),
        tags : Joi.array().items(Joi.string()).required(),
    });
        // 검증을 실시
    const result = schema.validate(ctx.request.body);

        //실패 후 에러 처리
    if(result.error) {
        ctx.status = 400;
        return;
    }
    const {title, body, tags} = ctx.request.body;

    const post  = new Post({
        title,
        body,
        tags,
    });
    try{
        await post.save();
        ctx.body = post;
    } catch (e){
        ctx.throw(500,e);
    }
};

페이지네이션 구현


가짜 데이터 생성


import Post from './models/post';

export default function createFakeData(){
    const posts = [...Array(40).keys()].map(i => ({
        title : `post #${i}`,
        body : `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sit amet ante sit amet dolor imperdiet scelerisque. Nunc egestas nunc vitae vestibulum aliquet. Donec tempor mollis lacus, et dictum tellus consectetur ut. Praesent sit amet pulvinar tellus. Integer vitae pretium ex. Morbi tempus nisl ut mi efficitur, non finibus dui dapibus. Aliquam odio lectus, sodales ac sollicitudin vel, elementum eu enim. Integer erat lacus, ullamcorper sit amet pulvinar ac, viverra a lacus. Nulla congue blandit ligula, in laoreet magna gravida sit amet. Curabitur pretium in tortor quis iaculis. Donec tellus augue, dapibus id justo non, volutpat condimentum nisl. In auctor quis augue non dictum. Nullam accumsan luctus aliquam. Vivamus ut ante sit amet lorem elementum consequat id ut purus.

Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Etiam orci urna, tincidunt nec nibh a, pellentesque sollicitudin purus. Sed finibus sodales mauris ac lobortis. Curabitur aliquam augue vel velit ultricies convallis. Praesent et tincidunt sapien. Suspendisse potenti. Aliquam erat volutpat. Donec leo velit, pellentesque sollicitudin laoreet vel, egestas non eros. Proin mauris nisl, condimentum eget fermentum sed, tristique nec erat. Donec aliquam accumsan mi quis posuere. Quisque nec augue congue, posuere arcu a, posuere mauris. Nunc tincidunt nisi et massa pretium ultrices. Maecenas commodo gravida eros vitae consectetur. Donec erat risus, varius non dapibus sit amet, iaculis vel tellus. Donec velit erat, auctor quis orci sit amet, condimentum mattis quam.

Donec eleifend, diam sit amet sodales euismod, metus felis consequat nibh, eget commodo felis nunc non erat. Cras a orci sed eros fringilla convallis in vitae nulla. Mauris eu efficitur enim. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Fusce id nisl odio. Fusce mollis erat quis augue tristique, non laoreet mauris lobortis. Vivamus venenatis vulputate purus vel imperdiet. Curabitur eu.

`,
        tags : ['fake, data'],
    }));

    Post.insertMany(posts,(err,docs)=>{
        console.log(docs);
    });
}

insertMany는 배열로 모델들을 받아와서 다 저장시켜주는 함수이고 이렇게 내보낸 createFackData를 한 번 호출해주면 40개의 더미 데이터들이 들어가는 것을 확인 할 수 있다

포스트 역정렬


블로그의 포스팅은 가장 최근에 쓴 글이 먼저 보인다

그런데 지금 만든 포스트들은 id로 오름차순 정렬이기 때문에 오래된 순서대로 보이고 있는데

list api에 가서 역정렬을 해주면된다

export const list = async ctx=>{
    try{
        const posts = await Post.find().sort({_id : -1}).exec();
        ctx.body = posts;
    } catch (e){
        ctx.throw(500,e);
    }
};
sort({ 정렬할 값 : 정렬 기준  })

정렬할 값에 id를 넣으면 id를 기준으로 정렬하고 그외의 다른 값을 넣으면 그 값을 기준으로 비교해서 정렬한다

정렬 기준에 1을 넣으면 오름차순, -1을 넣으면 내림차순으로 정렬한다

포스트 개수 제한


한 번에 너무 많은 포스팅들이 보이는 것은 비효율적이다

limit 메서드를 이용해서 그 수를 제한할 수 있다

export const list = async ctx=>{
    try{
        const posts = await Post.find()
            .sort({_id:-1})
            .limit(10)
            .exec();
        ctx.body = posts;
    } catch (e){
        ctx.throw(500,e);
    }
};

페이지 기능 구현


페이지 분할


skip 메서드를 이용해서 페이지 분할? 기능을 구현할 수 있는데

skip(숫자)로 데이터를 조회하면 숫자만큼 생략하고 데이터를 조회하게 된다

위에서 불러올 포스트의 갯수를 10개로 제한했으니 1페이지당 10개의 skip을 하면되는데

export const list = async ctx=>{
    const page = parseInt((ctx.query.page || 1),10);
    if( page < 1){
        ctx.status = 400;
        return;
    }
    try{
        const posts = await Post.find()
            .sort({_id:-1})
            .skip((page-1)*10)
            .lean()
            .exec();
        const postCount = await Post.countDocuments().exec();
        ctx.set('Last-Page', Math.ceil(postCount/10));
    } catch (e){
        ctx.throw(500,e);
    }
};

page 쿼리 값을 받아와서 동적으로 스킵해주면 페이지를 구현할 수 있다

마지막 페이지


그 밑에 구현한

    const postCount = await Post.countDocuments().exec();
    ctx.set('Last-Page', Math.ceil(postCount/10));

부분은 Post에 문서 갯수를 받아온 다음 10개로 나누어서 마지막 페이지의 정보를 얻은 뒤에

ctx.set으로 커스텀 헤더를 설정하고 있다

길이 제한


body의 길이가 너무 길면 다 보여줄 필요없이 일부만 보여주고 생략해야하는게 ux에 좋다

그래서 body의 내용을 수정해야하는데 find로 조회한 데이터는 mongoose 문서 인스턴스의 형태라 직접적으로 수정할 수가 없다

그래서 약간의 가공을 거쳐야하는데

...
    const posts = await Post.find()
        .sort({_id:-1})
        .skip((page-1)*10)
        .limit(10)
        .lean()
        .exec();
...

조회할 때 lean 메서드를 섞으면 데이터를 JSON 형태로 변환해서 가져올 수 있다

이렇게 가져온 데이터를

try{
        const posts = await Post.find()
            .sort({_id:-1})
            .skip((page-1)*10)
            .limit(10)
            .lean()
            .exec();
        const postCount = await Post.countDocuments().exec();
        ctx.set('Last-Page', Math.ceil(postCount/10));
        ctx.body = posts.map(post =>({
                ...post,
                body : post.body.length <200 ? post.body : `${post.body.slice(0,200)}...`,
            }));
    } catch (e){
        ctx.throw(500,e);
    }
};

200자로 잘라서 보여주면 끝이다