[nodejs]todoApp 만들기(express, nunjucks, mongodb,body-parser)
0. 목차
1. 개요
2. 기본세팅
3. HTML & CSS
4. CRUD
5. 정리
1. 개요
가장 기본이 되는 CRUD 만들어볼거다.
사용할 라이브러리는,
express, nunjucks, body-parser(exprss 내장 객체 대체가능)이며,
데이터 베이스는 noSQL인 mongoDB를 사용한다.
연휴기간중 mySQL 공부를 마치면 mySQL로 만드는 것도 포스팅 할듯..?
2. 기본 세팅
2-1. express
$ npm init -y
$ npm install express
터미널에 이거 두개 입력해서,
npm 설치하고,
npm을 통해 express 를 설치한다.
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('hello world');
});
app.listen(8000);
express 모듈을 require로 땡겨오고,
listen method로 8000번 포트를 열어준뒤,
localhost:8000/ 에 접속하면, 'hello world'가 랜더링 되게끔 해서
express 서버를 제대로 만들었는지 확인을 해본다.
이러면 아주 잘 된거임.
2-2. nunjucks
템플릿 엔진은 nunjucks를 사용할거다.
ejs 로 연습을 했었어서 ejs가 손에 익었는데
수업에선 nunjucks 쓴다고 하니 nunjucks 손에 익힐겸 nunjucks로 해봄.
$ npm install nunjucks
npm 통해서 nunjucks 설치하고,
const nunjucks = require('nunjucks');
app.set('view engine', 'html');
nunjucks.configure('views', { express: app });
요걸 입력해주면 된다.
하나하나 해석하면,
nunjucks 모듈 require로 땡겨오고,
app 인스턴스에 set method를 사용하여,
key는 view engine,
value는 html 인 property를 추가해준다.
그리고 nunjucks.configure의 첫번째 인수는
html을 담아둘 폴더의 이름을,
두번째 인수는, express 속성에 app을 연결해준다.
그리고 views라는 폴더를 하나 만들어서 이제 저 폴더 안에서 html 파일을 관리하면 된다.
2-3. body-parser
body-parser는 request 의 body를 읽는데 사용한다.
기존에는 body-parser라는 외부 라이브러리를 이용했는데,
최근에 express 업데이트를 통해서 express 내장 모듈로도 이용이 가능하다.
일단 외부 라이브러리인 body-parser를 이용하는 방법은
$ npm install body-parser
body-parser 설치하고,
app.use(bodyParser.urlencoded({ extended: true }));
요거 해주면 된다.
저 urlencoded의 인수인 extended는 중첩객체를 표시할것이나 말것이냐를 나타낸다.
true면 중첩객체 역시 나타내고
false면 중첩객체는 그냥 object로만 나타낼 것이다.
일단 true로 할거임
express 업데이트를 통해 body-parser 설치 없이도
완전히 같은 기능을 이용할 수 있는데,
app.use(express.urlencoded({ extended: true }));
따로 패키지 설치 없이 이것만 입력해주면 된다.
나는 그냥 외부 패키지 이용하는게 익숙하기도 하고,
안정성의 문제도 있다하니 그냥 body-parser를 쓰려한다.
2-4. mongodb
데이터베이스의 종류로는 noSQL 중 하나인 mongodb를 사용할거고,
매우매우 편하게 mongodb Atlas 사용할거임..
mongo db atlas는 로컬 컴퓨터에 db 설치 안하고,
클라우드형식으로 db를 관리 할 수 있게끔 해주는 서비스임
심지어 무료로도 사용 가능 ^^
가볍게 공부하는 용도로 사용하기 딱! 좋음
연휴 중에 mySQL 공부 하고 나면
mySQL 로 만드는 거 다시 포스팅 할거임.
그.. atlas 사용법은 다른 포스팅을 보는걸 추천함.
$ npm install mongodb
mongodb 설치하고,
const MongoClient = require('mongodb').MongoClient;
let db = null;
MongoClient.connect(
'Atlas connect url',
(error, client) => {
if (error) {
return console.log(error);
}
db = client.db('todoApp');
}
);
저거 입력하면됨, 저 Atlas connect url 부분에 아틀라스에서 따온 url입력해주면 됨.
2-5. 정적파일제공
으음... 이거 설명하는게 좀.. 애매한데..
우선 정적 파일이 뭔지 알아보자.
정적 파일이란 직접 값에 변화를 주지 않는 한 변하지 않는 파일 들을 의미.. 한다.
ㅎ.. 나도 뭔소린지 잘 모르겠음.
그냥 이미지 파일, 동영상파일, css파일, js파일 같은 애들을 정적파일이라고 하나보다. 뭐 그런가보다..
솔직히 잘 이해못했다.
근데 이걸 안하면, 이미지파일도 안불러와지고,
CSS 적용도 안되고, Javascript 도 적용 안됨 그러니까 그냥 무적권 해야되는 설정 중 하나임.
app.use(express.static(`${__dirname}/public`));
이거 한줄 추가해주고,
public 폴더 하나 만들어주면 끝!
여기까지가 기본적인 셋팅이다.
이 정도의 셋팅은 진짜 무지성으로 할 수 있을때 까지 계속 연습해야될듯..!
현재 전체 코드는
const express = require('express');
const nunjucks = require('nunjucks');
const bodyParser = require('body-parser');
const MongoClient = require('mongodb').MongoClient;
const app = express();
let db = null;
app.set('view engine', 'html');
nunjucks.configure('views', { express: app });
app.use(bodyParser.urlencoded({ extended: true }));
MongoClient.connect(
'Atlas connect url',
(error, client) => {
if (error) {
return console.log(error);
}
db = client.db('todoApp');
}
);
app.use(express.static(`${__dirname}/public`));
app.get('/', (req, res) => {
res.send('hello world');
});
app.listen(8000);
이렇다.
3. HTML & CSS
으...
이쁜게 목적이 아니니까
최대한 심플하게..
총 페이지는 4개의 페이지를 만들거다.
home,
list,
write,
edit
home 에는 나중에 계정 기능을 추가하면 사용할 수 있는,
로그인 창을 만들거다.
list 는 작성된 내용을 db에서 불러와 랜더링 할거고,
write 와 edit은 거의 동일한 형태로 구성될 듯 하다.
순서대로 home, list, write, edit 임
결코 열심히 css 를 하지 않겠다는 굳은 의지가 보인다..!
4. CRUD
4-1. Create
가장 먼저 write 페이지에서 submit을 하게 되면,
입력한 내용이 데이터 베이스에 등록되게끔 해보자.
<form action="/create" method="post">
<label for="todo">todo</label>
<input type="text" name="todo" id="todo" />
<label for="due_date">due date</label>
<input type="text" name="dueDate" id="due_date" />
<button type="submit">작성하기</button>
</form>
태그는 이렇게 짜 놓았다.
/create 로 post 되게 끔 해놓았으니가
server.js에서 create 에 post 요청이 들어 왔을 때 어떤 일이 생길지를 만들어주면 된다.
app.get('/write', (req, res) => {
res.render('write.html');
});
우선 write uri 에 접근하면 열심히 작성한 write.html 파일을 보여주도록 설정을 해놓고,
app.post('/create', (req, res) => {
console.log(req.body);
});
이걸 create 에 post 요청이 들어올 때,
req.body 를 출력하도록 해보았다.
write 페이지에서 내용을 입력하고 작성하기 버튼을 통해 submit 을 때려주면
터미널에 요 내용이 찍히는걸 확일 할 수 있다.
그러면 이 내용을 토대로 db에 저장을 해주면 될듯.
app.post('/create', (req, res) => {
db.collection('post').insertOne(
{
todo: req.body.todo,
dudDate: req.body.dueDate,
},
(error, result) => {
if (error) return console.log(error);
console.log('저장완료');
res.send('저장 완료');
}
);
});
이렇게 변경을 해주면,
내가 만들어 놓은 db에서 이름이 post인 collection 을 찾아서 첫번째 인수로 들어간 객체를 post collection 에 추가한다.
그리고 추가가 완료되면, 두번째 인수인 콜백 함수가 실행되는데,
에러가 발생하면 에러내용을 출력하고 함수가 종료되고,
에러가 발생하지 않으면, 터미널에 저장완료라는 문구를 출력하고
브라우저에도 저장완료라는 문구가 랜더링 되게 설정했다.

이렇게 작성하고 submit을 때리면,

저장완료가 랜더된 페이지와

터미널에도 저장완료가 출력 되었다.
그리고 db에도 역시

내가 입력한 내용이 내가 설정한 collection 에 잘 들어간걸 확인할 수 있다.
저 _id 라는 부분은 내가 따로 설정을 해주지 않으면
저런식으로 랜덤한 문자열이 입력이 되는데,
저 _id를 이용해서 delete 와 update를 해줘야하므로,
지금 미리 손을 봐두자.
나는 저 _id 란 값에 게시물의 넘버링을 해주고 싶다.
처음 등록된 글에는 0,
그 다음 글에는 1,
그다음 글에는 2 이런식으로
쭉쭉쭉
그걸위해 cluster에 counter라는 collection을 하나 만들었다.

이렇게 생긴 앤데,
totalPosts부분에 있는 숫자를
post collection 에 추가될 데이터의 _id 값으로 할당을 해준뒤,
저 숫자를 하나 늘려줄거다.
ㅎ..
app.post('/create', (req, res) => {
let _id = null;
db.collection('counter').findOne({ name: 'numOfPosts' }, (error, result) => {
if (error) return console.log(error);
_id = result.totalPosts;
console.log(_id);
});
db.collection('post').insertOne(
{
_id: _id,
todo: req.body.todo,
dudDate: req.body.dueDate,
},
(error, result) => {
if (error) return console.log(error);
console.log(_id, '저장완료');
res.send('저장 완료');
}
);
db.collection('counter').updateOne(
{ name: 'numOfPosts' },
{ $inc: { totalPosts: 1 } },
(error, result) => {
if (error) return console.log(error);
}
);
});
위에서 부터 하나씩 보면,
counter collection 에서 이름이 numOfPosts 인 데이터를 찾아서 result로 받고,
result.totalPosts 를 _id에 할당 해준다.
그리고 post collection 에 데이터를 추가해준다.
마지막으로 다시 numOfPosts를 찾아서 totalPosts 에 1을 더해준다.
이렇게 해주면..!
안됨 ㅎ
모든 코드들이 비동기적으로 처리되는데 코드가 처리되는 순서대로 실행되다 보니,
실행할때마다 순서가 랜덤하게 지정된다.
이럴때 쓸수 있는 방법을 최근에 공부했다.
https://kong-dev.tistory.com/120?category=991165
[Javascript] 콜백지옥과 프로미스
0. 목차 1. 콜백지옥 2. 프로미스 1. 콜백지옥 프로미스를 이해하기 위해선 콜백 지옥을 이해해야 한다. 콜백지옥을 이해하기 위해선 콜백펑션을 이해해야 한다. .. ㅎ https://kong-dev.tistory.com/116?categ
kong-dev.tistory.com
바로 프로미스
근데 프로미스를 이용해도
then의 체이닝이 연속적으로 이뤄지면 그거또한 콜백지옥의 형태로 보이기 때문에
https://kong-dev.tistory.com/121?category=991165
[Javascript] async 와 await
0. 목차 1. 개요 2. async 3. await 4. 응용..? 1. 개요 https://kong-dev.tistory.com/120 [Javascript] 콜백지옥과 프로미스 0. 목차 1. 콜백지옥 2. 프로미스 1. 콜백지옥 프로미스를 이해하기 위해선 콜백 지..
kong-dev.tistory.com
async 와 await 으로 조진다...!
app.post('/create', async (req, res) => {
try {
const counter = await db
.collection('counter')
.findOne({ name: 'numOfPosts' });
const _id = counter.totalPosts;
await db //
.collection('post')
.insertOne({
_id, //
todo: req.body.todo,
dueDate: req.body.dueDate,
});
await db //
.collection('counter')
.updateOne({ name: 'numOfPosts' }, { $inc: { totalPosts: 1 } });
res.send('저장완료');
} catch (err) {
console.log(err);
}
});
일단 코드가 깔-끔하다.

이렇게 입력을 하고 submit을 때리면..!
두근두근 설렌다..

일단 이쁘게 랜더는 됐고
이제 db를 확인해보면

totalPosts에 1이 늘어났고...!

post collction 에도 우리가 원하는 형태로 잘 들어왔다!

데이터 하나를 더 추가해보니 원하던 대로 _id 값이 하나 늘어난 것을 확인 할 수 있다!
그런데 일반적으로 todo를 작성하면
리스트 페이지로 이동을 하는게 일반적이다.
지금은 저장완료만 나오는데,
리스트 페이지로 이동하게끔
res.redirect를 해주면 된다.
res.redirect('http://localhost:8000/list');
res.send를 지우고 위에 코드로 변경을 해주면

요렇게 뜬다.
이제 list에 get 요청이 들어올 때 데이터를 읽어서 화면에 랜더해주면 된다!
4-2. Read
app.get('/list', (req, res) => {
res.render('list.html');
});
이렇게 해주면..
안됨
현재 list.html은 데이터베이스에 있는 내용을 전달받지 못하기 때문에,
내가 처음 마크업해놓은 내용만 갖고있다.
내가 원하는건 데이터베이스에 있는 내용을
html에서 읽어오길 원하는데
이때 사용할 수 있는게 템플릿 엔진이고
나는 많은 템플릿엔진 종류중 nunjucks를 사용할 것이다.
우선은 데이터베이스의 내용을 변수에 담아 html에 전달 해주는 것이 우선이다
app.get('/list', async (req, res) => {
try {
const postList = await db.collection('post').find().toArray();
res.render('list.html', { postList });
} catch (err) {
console.log(err);
}
});
이번에도 깔끔하게 async/await 으로 짰다.
post collection 에 있는 모든 데이터를 배열로 만들어서 postList라는 변수에 담은 뒤,
list.html로 전달 해주었다.
<main>
<ul>
{% for post in postList%}
<li class="todo_list">
<form action="/delete" method="post">
<input
type="text"
name="_id"
style="display: none"
value="{{ post._id }}"
/>
<p class="list_title">todo</p>
<p class="list_info">{{post.todo}}</p>
<p class="list_title">due date</p>
<p class="list_info">{{post.dueDate}}</p>
<div id="btn_cont">
<button type="button">수정하기</button>
<button type="submit">삭제하기</button>
</div>
</form>
</li>
{% endfor %}
</ul>
</main>
list.html을 이렇게 작성해 주니까
이제껏 입력했던 데이터들을 다 받아와서 랜더링을 해준다!
4-3. Delete
이제 저 todo list 의 삭제 하기 버튼을 누르면
데이터베이스에서 해당 데이터를 검색해서 삭제 할 수 있도록 만들어줄거다.
위에 있는 HTML을 보면 li 안에 form 태그가 있고,
삭제하기 버튼은 delete uri 에 post 요청을 하는 submit 버튼이다
그리고 display를 none으로 설정해놓은 _id가 req.body에 전송되므로,
데이터베이스에서 특정 _id를 검색해 해당하는 데이터를 삭제한 뒤,
다시 list 페이지로 돌아오게끔 만들어 주면 될듯하다.
app.post('/delete', async (req, res) => {
try {
const _id = Number(req.body._id);
await db.collection('post').deleteOne({ _id });
res.redirect('http://localhost:8000/list');
} catch (err) {
console.log(err);
}
});
req.body의 내용을 읽어올 때는
데이터타입을 string으로 읽어오기 때문에,
Number method를 이용해 데이터 타입을 number로 변환해줘야한다.
이점만 유의하면 간단하게 해결 할 수 있다.
4-4. Update
대망의 업데이트다.
수정하기 버튼의 동작순서는
수정하기 버튼 클릭
-> edit/:id 로 이동
-> edit/:id에서 수정버튼 클릭
-> update에 post 요청
-> 데이터베이스에서 데이터 변경
-> list로 redirect
이 될거다.
그럼 우선 버튼이 클릭 했을때
edit/:id 로 이동하게 하는 작업을 할거다.
이부분은 location.href method를 이용할거다.
<button type="button" onclick="udtBtnClick({{post._id}})">
수정하기
</button>
수정하기 버튼을 조금 손봐주고,
function udtBtnClick(idNum) {
const url = `http://localhost:8000/edit/${idNum}`;
location.href = url;
}
script에 요걸 추가 해주면
수정하기 버튼을 눌렀을 때 내가 원하는 사이트로 이쁘게 이동을 한다.
우린 이제 저 주소창에 있는 1을 따와야된다.
어떻게?
app.get('/edit/:id', (req, res) => {
console.log(req.params);
res.render('edit.html');
});
이렇게
console.log로 req.params를 찍어보면
요렇게 출력된다.
/edit/:어쩌고
이런식으로 uri를 지정하면,
edit/ 이후에 붙는 uri 가 req.params라는 객체에
key는 어쩌고
value는 uri 로 등록된다.
저 값을 이용해,
db에서 _id의 값과 일치하는 데이터를 검색해서
변수에 담아 html에 전송해주면 된다.
app.get('/edit/:id', async (req, res) => {
try {
const _id = Number(req.params.id);
const udtPost = await db.collection('post').findOne({ _id });
if (udtPost !== null) {
res.render('edit.html', { udtPost });
} else {
res.send('존재하지 않는 게시물 입니당');
}
} catch (err) {
res.send('존재하지 않는 게시물 입니다.');
console.log(err);
}
});
이렇게!
그리고 edit.html 파일을 조금 수정해주면
<main>
<form action="/edit" method="post">
<label for="todo">todo</label>
<input type="text" name="todo" id="todo" value="{{udtPost.todo}}" />
<label for="due_date">due date</label>
<input
type="text"
name="dueDate"
id="due_date"
value="{{udtPost.dueDate}}"
/>
<input type="hidden" name="_id" value="{{udtPost._id}}" />
<button type="submit">수정하기</button>
</form>
</main>
edit/1 에 접속하면
데이터베이스에서 _id 가 1인 데이터를 찾아서
변수에 담에 html에 전달 해주었고,
html은 전달받은 변수의 참조를 통해
_id 가 1인 데이터의 내용을 입력하여 랜더링 해준다.
이제 여기서 수정하기 버튼을 누르면
/edit으로 post 요청을 할것이고
post 요청을 받은 /edit 은 수정된 내용을 데이터베이스에 덮어씌울 것이다.
이 과정이 모두 끝나고 나면,
다시 list로 접속하도록 만들어주면 끝이다.
app.post('/edit', async (req, res) => {
try {
const _id = Number(req.body._id);
const todo = req.body.todo;
const dueDate = req.body.dueDate;
await db
.collection('post')
.updateOne({ _id }, { $set: { _id, todo, dueDate } });
res.redirect('http://localhost:8000/list');
} catch (err) {
console.log(err);
}
});
코드로 보는게 더 쉬울지도?
무튼 이렇게 돼있던걸
요렇게 바꾸고 submit을 때리면!
이렇게 수정된 내용을 보여주는 list 페이지로 이동한다.
그리고 당연하게도 데이터베이스의 데이터도 변경이 된것을 확인 할 수 있다.
5. 정리
음 한번 만들어 봤던걸
조금 다듬으면서,
블로그에 정리를 하면서 만들어보니까
확실히 내용이 머릿속에 한번더 입력이 되는 기분이다.
다만 데이터베이스를 다루는 부분이 너무 야매인거 같아서..
mySQL 을 공부한뒤 다시 한번 다룰 예정이다.
그리고 계정을 관리하는 법을 익히고 나면
이 포스팅에 추가로 올리던
아니면 새로운 글을 작성해서 링크를 붙이던 할것 같다.
아 그리고
db.collection('post') 와
db.collection('counter') 는 계속 반복적으로 사용을 하고있는데,
코드 윗부분에서 변수에 담아서 사용을 해도 괜찮겠다는 생각이 들었다.
이걸 하면서 다시한번 async / await 구문이 정리가 된거 같아서
여러모로 소득을 거둔것 같아 뿌듯하다.