Vanilla JS로 SPA 게시판 구현해보기!

JaeungE·2021년 12월 23일
5
post-thumbnail

친구와 함께 프로젝트를 진행하기 전에, 원활하게 작업을 진행하기 위해서 미니 프로젝트로 간단한 게시판을 만들어 보기로 했다.

평소에 기술의 기초 및 구조의 이해를 중요하게 생각했기 때문에, SPA 개발에 도움을 주는 react, vue, angular와 같은 libraryframework 를 써보기 전에 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"
  }

구형 브라우저의 지원을 위해 babelwebpack을 사용하였다.


프로젝트 구조

resource의 하위에 있는 dist 폴더에는 webpack을 통해 번들링 된 파일이, static 폴더에는 프로젝트에 필요한 각종 정적 파일들이 있다.

root 경로에는 패키지 정보와 babelwebpack에 필요한 설정 파일들이 존재한다.


설정 파일

babel.config.js

{
    "presets" : ["@babel/preset-env"],
    "plugins" : [["@babel/plugin-transform-runtime", { "corejs" : 3 }]]
}

기본적으로 preset@babel/preset-env을 사용했고, babel7 polyfill을 위해서@babel/plugin-transform-runtime 플러그인을 추가했다.


webpack.config.js

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 outputES6로 설정되어 있기 때문에, ES5로 재설정하기 위함이다.


API

back-end를 담당한 친구가 만든 API의 복사본이다. 링크는 아래와 같다.

[MiniBoard API]
https://equal-coaster-015.notion.site/MiniBoard-API-5ff8948555f84ef8b844272b47ad2842


코드

server

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 파일을 응답한다.


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>

cssjs 파일을 가져오는 것 외에는 기본적인 프레임만 존재하고, <div class="app-root"></div> 안에 동적으로 DOM Element가 추가될 예정이다.


js

main.js

모듈로 router.js를 사용하고 있다.

popstate 이벤트 발생시 router() 함수를 호출하고, javascript 문서가 로드되면 router() 함수를 호출하는 것 외에는 별 내용이 없다.

이 프로젝트에서 webpackEntry point가 되는 파일이다.


router.js

프로젝트의 핵심이 되는 모듈 코드의 일부다.

라우팅 코드는 Youtube 채널 dcode의 영상 Build a Single Page Application 을 참고해서 조금 변형하였다.


ajax.js

fetch, axios, jquery 등 비동기 통신을 위한 다양한 방법이 있지만 직접 비동기 처리 함수를 만들어 보고 싶어서 만든 함수다. 성공적으로 통신이 이뤄질 때 조금 뿌듯했다 ㅎㅎ😁

post 혹은 put 요청 시 객체 obj를 전달 인자로 함수를 호출하는데, json 형태로 변환해서 요청을 보내도록 만들었다.

ES8await 키워드를 사용하기 위해 promise 객체를 반환하게 했다.


views

AbstractView.js

모든 view의 부모가 되는 class 이다.

인스턴스를 생성할 때 생성자를 통해 params, url, convertDate() 프로퍼티를 가지기 때문에 이 class 를 상속받는 모든 class 에서 위의 프로퍼티를 사용할 수 있게 해두었다.

이 외의 각각의 view 들은 아래와 같은 구조로 되어있다.


AbstractView.js를 상속하여 getHtml() 함수를 view에 맞춰서 override 한다.

위에서 사용한 style 변수는 css-loaderlocalIdentName을 통해 유니크한 class name을 생성하기 위함이다.


view에서 보여줄 데이터를 ajax 요청을 통해 가져와서 동적으로 DOM Element를 만들어서 추가한다.


그리고 attachEvent() 함수를 override 해서 해당 view에서 필요한 Event Listener를 등록한다.


여기까지 프로젝트의 구조를 알아보았다. 이제 정상적으로 동작하는지 확인해 볼 차례다!😤



동작 확인

list

http://localhost:8082로 요청시 별도의 path가 없다면 router.js에 의해 http://localhost:8082/board로 요청하게 된다.

게시판 리스트를 보여주고, 게시물에 마우스 오버시 transition을 통해 제목의 색을 변경해준다.

하단에는 위처럼 페이징 처리가 되어있으며, "글쓰기" 버튼을 누르면 http://localhost:8082/write 페이지로 이동한다.

페이지 이동 전에 SPA 인지 확인하기 위해 console에 로그를 남겨보자.

이제 "글쓰기" 버튼을 눌러서 writeView를 불러와보자!


write

url이 변경되어도 console log가 그대로인 것을 볼 수 있다. 그럼 게시글 등록이 정상적으로 이뤄지는지 확인해보자.


성공적으로 생성된 것을 확인할 수 있다.

list page에서는 게시물 제목이 길어서 ...으로 축약되었다. 상세 보기를 통해 모든 내용이 제대로 등록되었는지 확인해보자!


detail

내용도 제대로 출력되는 것을 확인할 수 있다.

수정 및 삭제를 진행하기 전에 댓글 기능부터 확인해보자. 아무 댓글이나 등록해보겠다.

댓글을 등록하면 위처럼 댓글 내용과 우측에 X 버튼이 생긴다. 버튼을 누르게 되면 아래처럼 비밀번호를 입력하는 창이 생성된다.

잘못된 비밀번호 입력시 위처럼 alert 창을 띄우게 된다. 올바른 비밀번호를 입력해서 댓글이 삭제되는지 확인해보자.

정상적으로 댓글이 삭제된 것을 확인할 수 있다. 이제 수정 기능을 확인해보자.

수정 혹은 삭제 버튼을 누르면 위처럼 modal 창을 띄우게 된다.

댓글과 마찬가지로 잘못된 비밀번호 입력시 alert 창을 띄운다. 올바른 비밀번호를 입력해보자.


update

올바른 비밀번호를 입력하면 수정 페이지로 이동하게 된다.

이 화면에서 취소 버튼을 누르면 게시물 상세보기 페이지로 이동한다. 일단 내용을 아래처럼 수정해보자!

게시물 수정이 성공적으로 완료된 것을 볼 수 있다. 이제 삭제를 진행해보자!


delete

삭제 버튼을 누르면 수정과 동일하게 modal 창이 나오게 된다. 변경한 비밀번호인 1357을 입력해서 삭제를 진행해보자.

삭제가 진행되면 다시 list page로 이동하게 되고, 게시물이 성공적으로 삭제된 것을 확인할 수 있다.


이처럼 chrome 에서는 정상적으로 동작하는 것을 확인할 수 있었다.

구형 브라우저를 지원하기 위해 babelbabel polyfill 을 사용했으니, 황천에서 온 브라우저 IE11 에서도 작동하는지 확인해야 한다.

IE11 에서 접속해 보도록 하자!



IE11

IE11 에서도 list 정보를 정상적으로 가져온 것을 확인할 수 있다. 이참에 새 게시물도 한 번 등록해보자!

페이지 이동도 정상적으로 되고 있다. 아주 기쁘기 짝이 없다. 이제 게시물을 등록해보자!😊

???????????????????????????????????????????????????????????

뭐지?

어째서 이런일이......?

chrome 에서도 동일한지 확인해보자.

아.

아....................

위에서 보았듯이 IE11에서도 게시물 등록이 정상적으로 되긴 했으나, list page에는 반영이 되지 않은것을 볼 수 있다.

이런 문제가 발생한 이유는 IE11은 이미 방문한 url에 대해 get 요청은 cache를 사용하기 때문이다.

정말 대단한 브라우저가 아닐 수가 없다......

그래도 get 요청시 query string으로 현재 시간을 보낸다던가, request headerno-cache를 추가해서 간단히 해결이 가능한 문제라 크게 걱정하지 않아도 된다.

하지만 이미 이 프로젝트에서 원하는 것은 모두 얻었기 때문에 굳이 수정할 일은 없을 것 같다 ㅎㅎ..

정정, 무식한 DOM 조작을 모두 상태 기반 컴포넌트 구조로 리팩토링해 볼 생각이다.



0개의 댓글