친구와 함께 프로젝트를 진행하기 전에, 원활하게 작업을 진행하기 위해서 미니 프로젝트로 간단한 게시판을 만들어 보기로 했다.
평소에 기술의 기초 및 구조의 이해를 중요하게 생각했기 때문에, SPA 개발에 도움을 주는 react
, vue
, angular
와 같은 library
나 framework
를 써보기 전에 Vanilla JS
로 먼저 구현해보자고 생각했다.
이 프로젝트를 통해 이루고자 했던 목표는 모두 달성한 것 같아서 좋았고, 한 층 더 성장한 것을 느낄 수 있는 좋은 경험이었던 것 같다.
이제 어떻게 구현하였는지 하나씩 살펴보도록 하자!😆
프로젝트에 사용한 모듈은 다음과 같다.
"dependencies": {
"core-js": "^3.19.3",
"express": "^4.17.1"
},
"devDependencies": {
"@babel/cli": "^7.16.0",
"@babel/core": "^7.16.0",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/preset-env": "^7.16.4",
"@babel/runtime-corejs3": "^7.16.3",
"babel-loader": "^8.2.3",
"css-loader": "^6.5.1",
"mini-css-extract-plugin": "^2.4.5",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1"
}
구형 브라우저의 지원을 위해 babel
및 webpack
을 사용하였다.
resource
의 하위에 있는 dist
폴더에는 webpack
을 통해 번들링 된 파일이, static
폴더에는 프로젝트에 필요한 각종 정적 파일들이 있다.
root
경로에는 패키지 정보와 babel
과 webpack
에 필요한 설정 파일들이 존재한다.
{
"presets" : ["@babel/preset-env"],
"plugins" : [["@babel/plugin-transform-runtime", { "corejs" : 3 }]]
}
기본적으로 preset
은 @babel/preset-env
을 사용했고, babel7 polyfill
을 위해서@babel/plugin-transform-runtime
플러그인을 추가했다.
const path = require('path');
const miniCssExtract = require('mini-css-extract-plugin');
module.exports = {
mode : 'production',
entry : './resource/static/js/main.js',
output : {
filename : 'bundle.js',
path : path.resolve(__dirname, 'resource', 'dist', 'js')
},
module : {
rules : [
{
test : /\.css$/,
use : [ miniCssExtract.loader,
{
loader : 'css-loader',
options : {
modules : {
localIdentName : "[local]--[hash:base64:5]"
}
}
}
]
},
{
test : /\.js$/,
exclude : /node_modules/,
use : ['babel-loader']
}
]
},
plugins : [ new miniCssExtract( { filename : '../css/bundle.css' })],
target : ['web', 'es5']
}
css
를 별도의 파일로 분리하기 위해 mini-css-extract-plugin
을 사용했으며, 각 view
마다 css
를 로드 시 class name
이 중복되는 것을 피하고자 css-loader
의 옵션으로 localIdentName
을 사용했다.
가장 하단의 target
property는 webpack v5
부터 default output
이 ES6
로 설정되어 있기 때문에, ES5
로 재설정하기 위함이다.
back-end
를 담당한 친구가 만든 API
의 복사본이다. 링크는 아래와 같다.
[MiniBoard API]
https://equal-coaster-015.notion.site/MiniBoard-API-5ff8948555f84ef8b844272b47ad2842
server.js
const express = require('express');
const path = require('path');
const app = express();
app.use('/resource', express.static(path.resolve(__dirname, 'resource')));
app.get('/*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'resource', 'static', 'html', 'index.html'));
});
app.listen(8082, () => console.log('8082 port is running......'));
8082 포트의 모든 요청에 대한 응답으로 index.html
파일을 응답한다.
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="/resource/dist/css/reset.css">
<link rel="stylesheet" href="/resource/dist/css/bundle.css">
</head>
<body>
<div class="wrapper">
<header>
<h1><a href="/board" class="main-link">MiniBoard</a></h1>
</header>
<section>
<div class="app-root">
</div>
</section>
</div>
<script type="text/javascript" src="/resource/dist/js/bundle.js"></script>
</body>
</html>
css
와 js
파일을 가져오는 것 외에는 기본적인 프레임만 존재하고, <div class="app-root"></div>
안에 동적으로 DOM Element
가 추가될 예정이다.
main.js
모듈로 router.js
를 사용하고 있다.
popstate
이벤트 발생시 router()
함수를 호출하고, javascript
문서가 로드되면 router()
함수를 호출하는 것 외에는 별 내용이 없다.
이 프로젝트에서 webpack
의 Entry point
가 되는 파일이다.
router.js
프로젝트의 핵심이 되는 모듈 코드의 일부다.
라우팅 코드는 Youtube 채널 dcode의 영상 Build a Single Page Application 을 참고해서 조금 변형하였다.
ajax.js
fetch
, axios
, jquery
등 비동기 통신을 위한 다양한 방법이 있지만 직접 비동기 처리 함수를 만들어 보고 싶어서 만든 함수다. 성공적으로 통신이 이뤄질 때 조금 뿌듯했다 ㅎㅎ😁
post
혹은 put
요청 시 객체 obj
를 전달 인자로 함수를 호출하는데, json
형태로 변환해서 요청을 보내도록 만들었다.
ES8
의 await
키워드를 사용하기 위해 promise
객체를 반환하게 했다.
AbstractView.js
모든 view
의 부모가 되는 class
이다.
인스턴스를 생성할 때 생성자를 통해 params
, url
, convertDate()
프로퍼티를 가지기 때문에 이 class
를 상속받는 모든 class
에서 위의 프로퍼티를 사용할 수 있게 해두었다.
이 외의 각각의 view
들은 아래와 같은 구조로 되어있다.
AbstractView.js
를 상속하여 getHtml()
함수를 view
에 맞춰서 override
한다.
위에서 사용한 style
변수는 css-loader
의 localIdentName
을 통해 유니크한 class name
을 생성하기 위함이다.
view
에서 보여줄 데이터를 ajax
요청을 통해 가져와서 동적으로 DOM Element
를 만들어서 추가한다.
그리고 attachEvent()
함수를 override
해서 해당 view
에서 필요한 Event Listener
를 등록한다.
여기까지 프로젝트의 구조를 알아보았다. 이제 정상적으로 동작하는지 확인해 볼 차례다!😤
http://localhost:8082
로 요청시 별도의 path
가 없다면 router.js
에 의해 http://localhost:8082/board
로 요청하게 된다.
게시판 리스트를 보여주고, 게시물에 마우스 오버시 transition
을 통해 제목의 색을 변경해준다.
하단에는 위처럼 페이징 처리가 되어있으며, "글쓰기" 버튼을 누르면 http://localhost:8082/write
페이지로 이동한다.
페이지 이동 전에 SPA
인지 확인하기 위해 console
에 로그를 남겨보자.
이제 "글쓰기" 버튼을 눌러서 writeView
를 불러와보자!
url
이 변경되어도 console log
가 그대로인 것을 볼 수 있다. 그럼 게시글 등록이 정상적으로 이뤄지는지 확인해보자.
성공적으로 생성된 것을 확인할 수 있다.
list page
에서는 게시물 제목이 길어서 ...
으로 축약되었다. 상세 보기를 통해 모든 내용이 제대로 등록되었는지 확인해보자!
내용도 제대로 출력되는 것을 확인할 수 있다.
수정 및 삭제를 진행하기 전에 댓글 기능부터 확인해보자. 아무 댓글이나 등록해보겠다.
댓글을 등록하면 위처럼 댓글 내용과 우측에 X
버튼이 생긴다. 버튼을 누르게 되면 아래처럼 비밀번호를 입력하는 창이 생성된다.
잘못된 비밀번호 입력시 위처럼 alert
창을 띄우게 된다. 올바른 비밀번호를 입력해서 댓글이 삭제되는지 확인해보자.
정상적으로 댓글이 삭제된 것을 확인할 수 있다. 이제 수정 기능을 확인해보자.
수정 혹은 삭제 버튼을 누르면 위처럼 modal
창을 띄우게 된다.
댓글과 마찬가지로 잘못된 비밀번호 입력시 alert
창을 띄운다. 올바른 비밀번호를 입력해보자.
올바른 비밀번호를 입력하면 수정 페이지로 이동하게 된다.
이 화면에서 취소 버튼을 누르면 게시물 상세보기 페이지로 이동한다. 일단 내용을 아래처럼 수정해보자!
게시물 수정이 성공적으로 완료된 것을 볼 수 있다. 이제 삭제를 진행해보자!
삭제 버튼을 누르면 수정과 동일하게 modal
창이 나오게 된다. 변경한 비밀번호인 1357
을 입력해서 삭제를 진행해보자.
삭제가 진행되면 다시 list page
로 이동하게 되고, 게시물이 성공적으로 삭제된 것을 확인할 수 있다.
이처럼 chrome
에서는 정상적으로 동작하는 것을 확인할 수 있었다.
구형 브라우저를 지원하기 위해 babel
및 babel polyfill
을 사용했으니, 황천에서 온 브라우저 IE11
에서도 작동하는지 확인해야 한다.
IE11
에서 접속해 보도록 하자!
IE11
에서도 list 정보를 정상적으로 가져온 것을 확인할 수 있다. 이참에 새 게시물도 한 번 등록해보자!
페이지 이동도 정상적으로 되고 있다. 아주 기쁘기 짝이 없다. 이제 게시물을 등록해보자!😊
???????????????????????????????????????????????????????????
뭐지?
어째서 이런일이......?
chrome
에서도 동일한지 확인해보자.
아.
아....................
위에서 보았듯이 IE11
에서도 게시물 등록이 정상적으로 되긴 했으나, list page
에는 반영이 되지 않은것을 볼 수 있다.
이런 문제가 발생한 이유는 IE11은 이미 방문한 url
에 대해 get
요청은 cache를 사용하기 때문이다.
정말 대단한 브라우저가 아닐 수가 없다......
그래도 get
요청시 query string
으로 현재 시간을 보낸다던가, request header
에 no-cache
를 추가해서 간단히 해결이 가능한 문제라 크게 걱정하지 않아도 된다.
하지만 이미 이 프로젝트에서 원하는 것은 모두 얻었기 때문에 굳이 수정할 일은 없을 것 같다 ㅎㅎ..
정정, 무식한 DOM 조작을 모두 상태 기반 컴포넌트 구조로 리팩토링해 볼 생각이다.