Vue.js 입문
개발일기
최근 프론트에 부쩍 관심이 늘었다.
백엔드가 많이 능숙해져서 생산성이 폭발적으로 늘어난데 비해, 상대적으로 화면구성 속도가 너무 빈약하다고 느끼고 있기 때문이다.
더불어 JQuery
로 수백 수천 줄의 코드들을 작성하고, 수정하다 보면 너무 피로하다.
그래서 모던 자바스크립트
와 프론트 프레임워크
에 계속 관심을 기울이다가 이번 주말에 본격적으로 공부를 시작했다.
JQuery
로 작성된 토이 프로젝트를 모던 자바스크립트
로 마이그레이션 하고, 이를 다시 Vue.js
로 마이그레이션 해봤다.
✅ JQuery
const CONTEXT_PATH = "";
let ajaxResponse;
let allVisitors;
let dau;
/**
* @author shirohoo
* 페이지 초기화
*/
$(function () {
$.ajax({
url : "/boards?page=0&size=10",
type : "GET",
contentType: "application/x-www-form-urlencoded;charset=utf-8",
dataType : "json",
success : function (data) {
ajaxResponse = data.pages;
allVisitors = data.allVisitors;
dau = data.dau;
listRendering()
},
error : function () {
alert("Error. 관리자에게 문의하십시오.");
},
});
});
function requestAjax(url, param) {
try {
$.ajax({
url : url + param,
type : "GET",
contentType: "application/x-www-form-urlencoded;charset=utf-8",
dataType : "json",
success : function (data) {
ajaxResponse = data.pages;
allVisitors = data.allVisitors;
dau = data.dau;
listRendering()
},
error : function () {
alert("Error. 관리자에게 문의하십시오.");
},
});
}
catch (e) {
alert("[requestAjax] :: " + e.message);
}
}
/**
* 셀렉트 박스 회사 선택시 검색
*/
function selectedCompany() {
$('#company').val($('#selectCompany').val());
searchPost();
}
/**
* 각 페이지 버튼에 페이지 이동기능 추가
* @param selectedPageNum 이동하려는 페이지 번호
* @param size 보여주려는 목록의 갯수
*/
function pageMove(selectedPageNum, size) {
try {
searchResultList(selectedPageNum, size);
window.scrollTo({
top: 0, left: 0, behavior: 'smooth'
});
}
catch (e) {
alert("[pageMove] :: " + e.message);
}
}
/**
* 선택한 페이지에 해당하는 게시글을 불러옴
* @param selectedPageNum 선택한 페이지
* @param size 보여주려는 목록의 갯수
*/
function searchResultList(selectedPageNum, size) {
try {
let url = CONTEXT_PATH + "/boards?";
let param = $('#searchForm :input').filter(function (idx, element) {
return $(element).val() != '';
}).serialize();
param += param == "" ? "" : "&";
param += "page=" + selectedPageNum + "&size=" + size;
requestAjax(url, param);
}
catch (e) {
alert("[searchResultList] :: " + e.message);
}
}
/**
* 검색 버튼 클릭시 호출될 함수
*/
function searchPost() {
try {
let url = CONTEXT_PATH + "/boards?";
let currentPageNum = $('#currentPageNum').val() == "" ? 0 : $('#currentPageNum').val();
let size = $('#renderingCount').val();
let param = $('#searchForm :input').filter(function (idx, element) {
return $(element).val() != '';
}).serialize();
param += param == "" ? "" : "&";
param += "page=" + currentPageNum + "&size=" + size;
requestAjax(url, param);
}
catch (e) {
alert("[searchResultList] " + e.message);
}
}
/**
* 게시글 목록 렌더링
*/
function listRendering() {
try {
$('#listTbody').empty();
$('.SHOW-allVisitors').empty();
$('.SHOW-allVisitors').text(numberFormat(allVisitors) + ' 명');
$('.SHOW-DAU').empty();
$('.SHOW-DAU').text(numberFormat(dau) + ' 명');
let postList = ajaxResponse.content;
if (postList.length <= 0) {
let eleTr = $("<tr />");
let eleTd = $("<td />")
.css("text-align", "center")
.css("width", "100%")
.text("No matching search results found");
$(eleTr).append(eleTd);
$('#listTbody').append(eleTr);
} else {
$.each(postList, function () {
let regDateArray = this.regDate.split('-');
let regDate = new Date(regDateArray[0], regDateArray[1] - 1, regDateArray[2])
let nowDate = new Date();
let betweenDay = Math.floor((nowDate.getTime() - regDate.getTime()) / 1000 / 60 / 60 / 24);
let eleTr = $("<tr class='graph_tr1'/>")
.append($("<td style='text-align:center'/>").append($("<img src=" + this.imgPath + " height='48px' width='96px' title='" + this.company + "'/>")))
.append($("<td style='text-align:center'/>").append($("<span/>").attr("class", (betweenDay == 0 ? "badge badge-danger" : ""))
.text((betweenDay == 0 ? "Today" : "")))
.css("text-align", "left")
.css("font-weight", "bold")
.css("color", "skyblue")
.append($("<a/>").attr("href", this.link)
.attr("target", "blank")
.text(" " + this.title)))
.append($("<td style='text-align:center'/>").text(this.regDate));
$('#listTbody').append(eleTr);
});
}
$('#totalResultCount').empty().append("TOTAL : " + ajaxResponse.totalElements);
renderingPagingArea();
}
catch (e) {
alert("[listRendering] " + e.message);
}
}
function renderingPagingArea() {
try {
$('#pagingArea').empty();
if (ajaxResponse.first != true) {
let prePagebutton = $("<li/>").attr("class", "page-item")
.append(
$("<a/>").attr("class", "page-link")
.attr("href", "javascript:pageMove('"
+ (ajaxResponse.pageable.pageNumber - 1)
+ "', " + ajaxResponse.size + ");")
.text("<")
);
$('#pagingArea').append(prePagebutton);
}
let endPage = Math.ceil((ajaxResponse.pageable.pageNumber + 1) / 10.0) * 10 - 1;
let trueEndPage = Math.ceil(ajaxResponse.totalElements / ajaxResponse.size);
let startPage = endPage - 9;
if (trueEndPage <= endPage) endPage = trueEndPage - 1;
for (let i = startPage; i <= endPage; i++) {
let pageNumButton = $("<li/>").attr("class", "page-item");
if (ajaxResponse.pageable.pageNumber == i) {
$(pageNumButton).attr("class", "page-item active");
}
$(pageNumButton).append(
$("<a/>").attr("class", "page-link")
.attr("href", "javascript:pageMove('"
+ i
+ "', " + ajaxResponse.size + ");")
.text(i + 1)
);
$('#pagingArea').append(pageNumButton);
}
if (ajaxResponse.last != true) {
let nextPagebutton = $("<li/>").attr("class", "page-item")
.append(
$("<a/>").attr("class", "page-link")
.attr("href", "javascript:pageMove('"
+ (ajaxResponse.pageable.pageNumber + 1)
+ "', " + ajaxResponse.size + ");")
.text(">")
);
$('#pagingArea').append(nextPagebutton);
}
}
catch
(e) {
alert("[renderingPagingArea] :: " + e.message);
}
}
function resetSearchForm(searchFormId) {
try {
$("#" + searchFormId)[0].reset();
$('#company').val("");
searchPost();
}
catch (e) {
alert("[ resetSearchForm() ] :: " + e.message);
}
}
function enterKeyup() {
if (event.keyCode == 13) {
searchPost();
}
}
function numberFormat(num) {
let regexp = /\B(?=(\d{3})+(?!\d))/g;
return num.toString().replace(regexp, ',');
}
✅ 모던 JavaScript
'use strict';
/**
* @author shirohoo
* 페이지 초기화
*/
(function init() {
let url = '/boards?';
let param = 'page=0&size=10';
apiFetch(url, param, callbackDataBinding);
})();
const CONTEXT_PATH = '';
let response = {
pages : '',
visitorsOfReduce: '',
visitorsOfDay : ''
};
function apiFetch(url, param, callback) {
fetch(url + param)
.then(res => {
if (res.status === 200) return res.json()
})
.then(data => callback(data))
.catch(() => alert('400, Bad Request'));
}
function callbackDataBinding(data) {
response.pages = data.pages;
response.visitorsOfReduce = data.visitorsOfReduce;
response.visitorsOfDay = data.visitorsOfDay;
renderingBoardArea()
}
/**
* 검색 시 엔터키 감지
*/
function enterKeyUp() {
if (event.keyCode == 13) {
search();
}
}
/**
* 숫자 3자리마다 ,추가
*/
function formatNumber(num) {
let regexp = /\B(?=(\d{3})+(?!\d))/g;
return num.toString()
.replace(regexp, ',');
}
/**
* 초기화 버튼
*/
function resetSearchForm() {
try {
document.querySelector('#searchForm').reset();
document.querySelector('#company').value = '';
search();
}
catch (e) {
alert(`[resetSearchForm] :: ${e.message}`);
}
}
/**
* 검색 버튼 클릭시 호출될 함수
*/
function search() {
try {
let url = `${CONTEXT_PATH}/boards?`;
let param = new URLSearchParams();
let searchConditions = Array.from(
document.querySelector('#searchForm')
.getElementsByTagName('input')
).filter((element) => {
return element.id != '';
});
for (const conditions of searchConditions) {
param.append(conditions.name, conditions.value);
}
let currentPage = document.querySelector('#currentPageNum').value == ''
? 0 : document.querySelector('#currentPageNum').value;
let size = document.querySelector('#renderingCount').value;
param += param == '' ? '' : '&';
param += `page=${currentPage}&size=${size}`;
apiFetch(url, param, callbackDataBinding);
}
catch (e) {
alert(`[search] :: ${e.message}`);
}
}
/**
* 셀렉트 박스 회사 선택시 검색
*/
function selectedCompany() {
let company = document.querySelector('#company');
let selectCompany = document.querySelector('#selectCompany');
company.value = selectCompany.value;
search();
}
/**
* 각 페이지 버튼에 페이지 이동기능 추가
* @param targetPage 이동하려는 페이지
* @param size 보여주려는 목록의 갯수
*/
function pageMove(targetPage, size) {
try {
getPage(targetPage, size);
window.scrollTo({
top : 0,
left : 0,
behavior: 'smooth'
});
}
catch (e) {
alert(`[pageMove] :: ${e.message}`);
}
}
/**
* 선택한 페이지에 해당하는 게시글을 불러옴
* @param selectedPage 선택한 페이지
* @param size 보여주려는 목록의 갯수
*/
function getPage(selectedPage, size) {
try {
let url = `${CONTEXT_PATH}/boards?`;
let param = new URLSearchParams();
let searchConditions = Array.from(
document.querySelector('#searchForm')
.getElementsByTagName('input')
).filter((element) => {
return element.id != '';
});
for (const ele of searchConditions) {
param.append(ele.name, ele.value);
}
param += param == '' ? '' : '&';
param += `page=${selectedPage}&size=${size}`;
apiFetch(url, param, callbackDataBinding);
}
catch (e) {
alert(`[getPage] :: ${e.message}`);
}
}
/**
* 게시글 목록 렌더링
*/
function renderingBoardArea() {
try {
// 게시판 초기화
let listTbody = document.querySelector('#boards');
while (listTbody.firstChild) {
listTbody.removeChild(listTbody.firstChild);
}
// 누적 방문자 초기화
let allVisitors = document.querySelector('.SHOW-allVisitors');
while (allVisitors.firstChild) {
allVisitors.removeChild(allVisitors.firstChild);
}
allVisitors.textContent = `${formatNumber(response.visitorsOfReduce)} 명`;
// 오늘 방문자 초기화
let dau = document.querySelector('.SHOW-DAU');
while (dau.firstChild) {
dau.removeChild(dau.firstChild);
}
dau.textContent = `${formatNumber(response.visitorsOfDay)} 명`;
// 렌더링
let posts = response.pages.content;
if (posts.length <= 0) {
let eleTr = $('<tr />');
let eleTd = $('<td />')
.css('text-align', 'center')
.css('width', '100%')
.text('No matching search results found');
$(eleTr).append(eleTd);
$('#boards').append(eleTr);
} else {
$.each(posts, function () {
let regDateArray = this.regDate.split('-');
let regDate = new Date(regDateArray[0], regDateArray[1] - 1, regDateArray[2])
let nowDate = new Date();
let betweenDay = Math.floor((nowDate.getTime() - regDate.getTime()) / 1000 / 60 / 60 / 24);
let eleTr = $("<tr class='graph_tr1'/>").append($(`<td style="text-align:center"/>`)
.append($(`<img src="${this.imgPath}" height="48px" width="96px" title="${this.company}" />`)))
.append($(`<td style="text-align:center"/>`)
.append($('<span/>')
.attr('class', (betweenDay == 0 ? 'badge badge-danger' : ''))
.text((betweenDay == 0 ? 'Today' : '')))
.css('text-align', 'left')
.css('font-weight', 'bold')
.css('color', 'skyblue')
.append($("<a/>")
.attr('href', this.link)
.attr('target', 'blank')
.text(' ' + this.title)))
.append($(`<td style="text-align:center"/>`)
.text(this.regDate));
$('#boards').append(eleTr);
});
}
$('#totalResultCount').empty().append(`TOTAL : ${response.pages.totalElements}`);
renderingPagingArea();
}
catch (e) {
alert(`[listRendering] :: ${e.message}`);
}
}
/**
* 페이지네이션 렌더링
*/
function renderingPagingArea() {
try {
$('#pagingArea').empty();
if (response.pages.first != true) {
let prePagebutton = $('<li/>').attr('class', 'page-item')
.append($('<a/>')
.attr('class', 'page-link')
.attr('href', `javascript:pageMove(${(response.pages.pageable.pageNumber - 1)}, ${response.pages.size});`)
.text('<')
);
$('#pagingArea').append(prePagebutton);
}
let endPage = Math.ceil((response.pages.pageable.pageNumber + 1) / 10.0) * 10 - 1;
let trueEndPage = Math.ceil(response.pages.totalElements / response.pages.size);
let startPage = endPage - 9;
if (trueEndPage <= endPage) {
endPage = trueEndPage - 1;
}
for (let i = startPage; i <= endPage; i++) {
let pageNumButton = $("<li/>").attr('class', 'page-item');
if (response.pages.pageable.pageNumber == i) {
$(pageNumButton).attr('class', 'page-item active');
}
$(pageNumButton).append($('<a/>')
.attr('class', 'page-link')
.attr('href', `javascript:pageMove(${i}, ${response.pages.size});`)
.text(i + 1)
);
$('#pagingArea').append(pageNumButton);
}
if (response.pages.last != true) {
let nextPageButton = $('<li/>').attr('class', 'page-item')
.append($("<a/>").attr('class', 'page-link')
.attr('href',
`javascript:pageMove(${(response.pages.pageable.pageNumber + 1)}, ${response.pages.size});`
)
.text('>')
);
$('#pagingArea').append(nextPageButton);
}
}
catch (e) {
alert(`[renderingPagingArea] :: ${e.message}`);
}
}
✅ Vue.js (1차)
'use strict';
const app = new Vue({
el : '#app',
data: {
CONTEXT : '',
response: {
pages : '',
visitorsOfReduce: '',
visitorsOfDay : ''
},
pager : {},
},
mounted: function () {
let url = '/boards?';
let param = 'page=0&size=10';
this.apiFetch(url, param, this.callbackDataBinding);
},
methods: {
apiFetch(url, param, callback) {
fetch(url + param)
.then(res => {
if (res.status === 200) return res.json()
})
.then(data => callback(data))
.catch(() => alert('400, Bad Request'));
},
callbackDataBinding(data) {
this.response.pages = data.pages;
this.response.visitorsOfReduce = data.visitorsOfReduce;
this.response.visitorsOfDay = data.visitorsOfDay;
this.pager = this.setPage(data.pages.pageable.pageNumber, data.pages.size, data.pages.totalElements);
},
setPage(currentPage, size, total) {
let endPage = Math.ceil((currentPage + 1) / 10.0) * 10 - 1;
let trueEndPage = Math.ceil(total / size);
let startPage = endPage - 9;
if (trueEndPage <= endPage) {
endPage = trueEndPage - 1;
}
let index = [];
for (let i = startPage; i <= endPage; i++) {
index.push(i);
}
return {
index : index,
currentPage: currentPage,
startPage : startPage,
endPage : endPage,
trueEndPage: trueEndPage,
size : size,
total : total
};
},
enterKeyUp() {
if (event.keyCode == 13) {
this.search();
}
},
formatNumber(num) {
let regexp = /\B(?=(\d{3})+(?!\d))/g;
return num.toString().replace(regexp, ',');
},
resetSearchForm() {
document.querySelector('#searchForm').reset();
document.querySelector('#company').value = '';
this.search();
},
selectedCompany() {
let company = document.querySelector('#company');
let selectCompany = document.querySelector('#selectCompany');
company.value = selectCompany.value;
this.search();
},
search() {
let url = `${this.CONTEXT}/boards?`;
let param = new URLSearchParams();
let searchConditions = Array.from(
document.querySelector('#searchForm')
.getElementsByTagName('input')
).filter((element) => {
return element.id != '';
});
for (const conditions of searchConditions) {
param.append(conditions.name, conditions.value);
}
let currentPage = document.querySelector('#currentPageNum').value == '' ? 0 : document.querySelector('#currentPageNum').value;
let size = document.querySelector('#renderingCount').value;
param += param == '' ? '' : '&';
param += `page=${currentPage}&size=${size}`;
this.apiFetch(url, param, this.callbackDataBinding);
},
isBetweenDay(regDate) {
let regDateArray = String(regDate).split('-');
let date = new Date(regDateArray[0], regDateArray[1] - 1, regDateArray[2])
let nowDate = new Date();
let betweenDay = Math.floor((nowDate.getTime() - date.getTime()) / 1000 / 60 / 60 / 24);
return betweenDay === 0;
},
pageMove(targetPage, size) {
let url = `${this.CONTEXT}/boards?`;
let param = new URLSearchParams();
let searchConditions = Array.from(
document.querySelector('#searchForm')
.getElementsByTagName('input')
).filter((element) => {
return element.id != '';
});
for (const ele of searchConditions) {
param.append(ele.name, ele.value);
}
param += param == '' ? '' : '&';
param += `page=${targetPage}&size=${size}`;
this.apiFetch(url, param, this.callbackDataBinding);
},
},
});
✅ Vue.js (2차)
'use strict';
const app = new Vue({
el : '#app',
data : {
search : {
page : 0,
size : 10,
company: '',
title : ''
},
contents : {},
pager : {},
visitorsOfReduce: 0,
visitorsOfDay : 0
},
mounted: function () {
this.$nextTick(function () {
this.findContents();
});
},
methods: {
findContents(page) {
if (page !== undefined) this.search.page = page;
let query = Object.keys(this.search)
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(this.search[k]))
.join('&');
let url = '/boards?' + query;
fetch(url)
.then(res => {
if (res.status === 200) return res.json()
})
.then(data => {
this.contents = data.pages.content;
this.visitorsOfReduce = data.visitorsOfReduce;
this.visitorsOfDay = data.visitorsOfDay;
this.pager = this.setPage(data.pages);
})
.catch(() => alert('400, Bad Request'));
},
setPage(pages) {
let endPage = Math.ceil((pages.pageable.pageNumber + 1) / 10.0) * 10 - 1;
let startPage = endPage - 9;
if (pages.totalPages <= endPage) {
endPage = pages.totalPages - 1;
}
let index = [];
for (let i = startPage; i <= endPage; i++) {
index.push(i);
}
return {
index : index,
size : pages.size,
first : pages.first,
last : pages.last,
currentPage : pages.pageable.pageNumber,
totalPages : pages.totalPages,
totalElements: pages.totalElements
};
},
formatNumber(num) {
let regexp = /\B(?=(\d{3})+(?!\d))/g;
return num.toString().replace(regexp, ',');
},
enterKeyUp() {
if (event.keyCode === 13) {
this.findContents();
}
},
resetSearchForm() {
this.search.page = 0;
this.search.size = 10;
this.search.company = '';
this.search.title = '';
this.findContents();
},
isBetweenDay(regDate) {
let regDateArray = String(regDate).split('-');
let date = new Date(regDateArray[0], regDateArray[1] - 1, regDateArray[2])
let nowDate = new Date();
let betweenDay = Math.floor((nowDate.getTime() - date.getTime()) / 1000 / 60 / 60 / 24);
return betweenDay === 0;
},
},
});
✅ 후기
일단 코드가 확연히 줄어들긴 했는데, 양방향 바인딩
, 템플릿
, 컴포넌트
, 상태관리
등의 기능을 적극 활용한다면 여기서 훨씬 더 좋은 코드가 될 거라는 생각이 들었다.
결국 Vue.js
를 다루는 실력이 아직 비루한 게 문제인 것 같다.
Vue.js
를 처음 공부하고 사용해보니 ORM
과 비슷한 느낌이 든다.
JPA 같은 경우 RDB를 가상의 인메모리 DB(영속성 컨텍스트)와 연결하고 이를 조작하는 방식으로 RDB를 사용하더라도 객체지향적인 프로그래밍이 가능하게 해 준다.
이와 비슷하게 Vue.js
는 가상의 DOM
을 만들어 DOM과 연결하고 이를 조작하는 방식으로 돌아가는 듯했다.
항상 이런 신기술을 학습할 때 가장 어려운 것은 급격한 사고의 전환이 필요하다는 점이다.
사용하는 기술은 바뀌었는데 작업하던 방식은 기존방식이라면 이런 좋은 기술들을 도입하더라도 눈에 띄는 효과를 보기가 어려운 것 같다.
그러니 신기술을 학습하고 도입할 땐 구조와 원리에 대해 깊게 학습하고 이를 의식하며 점진적으로 체화시켜 나가야 하겠다.