Vue.js 입문

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과 연결하고 이를 조작하는 방식으로 돌아가는 듯했다.

항상 이런 신기술을 학습할 때 가장 어려운 것은 급격한 사고의 전환이 필요하다는 점이다.

사용하는 기술은 바뀌었는데 작업하던 방식은 기존방식이라면 이런 좋은 기술들을 도입하더라도 눈에 띄는 효과를 보기가 어려운 것 같다.

 

그러니 신기술을 학습하고 도입할 땐 구조와 원리에 대해 깊게 학습하고 이를 의식하며 점진적으로 체화시켜 나가야 하겠다.

 


© 2022. All rights reserved.