HTML Table을 CSV로 다운로드하기

들어가며

웹 개발을 하다보면 <table>의 내용물을 모두 csv 파일로 받게 해달라는 요구사항이 종종 생깁니다.

가장 일반적인 방법은 csv 형태로 파일을 받을 수 있는 API를 서버가 제공해주는 방법입니다. (백엔드 개발자에게 일을 시킵시다.)

하지만 csv를 던져주는 API 서버가 없다면 프론트에서 보여지는 <table>만이라도 csv로 만들어줘야 합니다.

이번 글은 이럴때 쓰는 방법입니다.

소스코드

우선 이렇게 생긴 HTML이 있다고 생각해 봅시다.

본문이라고는 #, title, content가 들어있는 자그마한 <table> 하나가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<head lang="ko">
<meta charset="utf-8">
<title>빈 HTML</title>
</head>
<body>
<table id="mytable">
<thead>
<tr>
<th>#</th>
<th>title</th>
<th>content</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Lorem Ipsum</td>
<td>로렘 입섬은 빈칸을 채우기 위한 문구입니다.</td>
</tr>
<tr>
<td>2</td>
<td>Hello World</td>
<td>헬로 월드는 언어를 배우기 시작할때 화면에 표준 출력을 할때 주로 사용하는 문구입니다.</td>
</tr>
</tbody>
</table>

<button id="csvDownloadButton">CSV 다운로드 받기</button>
</body>
</html>

테이블 엘리먼트의 id는 mytable이고, CSV 다운로드 버튼의 id는 csvDownloadButton 입니다.

이제 JS를 조금 추가해봅시다. ES6/ES5에 따라 선택해 사용해주세요.

아래 코드를 <script></script> 태그 사이에 넣어 </body> 바로 앞에 넣어주세요.

ES6을 사용할 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class ToCSV {
constructor() {
// CSV 버튼에 이벤트 등록
document.querySelector('#csvDownloadButton').addEventListener('click', e => {
e.preventDefault()
this.getCSV('mycsv.csv')
})
}

downloadCSV(csv, filename) {
let csvFile;
let downloadLink;

// CSV 파일을 위한 Blob 만들기
csvFile = new Blob([csv], {type: "text/csv"})

// Download link를 위한 a 엘리먼스 생성
downloadLink = document.createElement("a")

// 다운받을 csv 파일 이름 지정하기
downloadLink.download = filename;

// 위에서 만든 blob과 링크를 연결
downloadLink.href = window.URL.createObjectURL(csvFile)

// 링크가 눈에 보일 필요는 없으니 숨겨줍시다.
downloadLink.style.display = "none"

// HTML 가장 아래 부분에 링크를 붙여줍시다.
document.body.appendChild(downloadLink)

// 클릭 이벤트를 발생시켜 실제로 브라우저가 '다운로드'하도록 만들어줍시다.
downloadLink.click()
}

getCSV(filename) {
// csv를 담기 위한 빈 Array를 만듭시다.
const csv = []
const rows = document.querySelectorAll("#mytable table tr")

for (let i = 0; i < rows.length; i++) {
const row = [], cols = rows[i].querySelectorAll("td, th")

for (let j = 0; j < cols.length; j++)
row.push(cols[j].innerText)

csv.push(row.join(","))
}

// Download CSV
this.downloadCSV(csv.join("\n"), filename)
}
}

document.addEventListener('DOMContentLoaded', e => {
new ToCSV()
})

ES5를 사용하실 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function downloadCSV(csv, filename) {
var csvFile;
var downloadLink;

// CSV 파일을 위한 Blob 만들기
csvFile = new Blob([csv], {type: "text/csv"})

// Download link를 위한 a 엘리먼스 생성
downloadLink = document.createElement("a")

// 다운받을 csv 파일 이름 지정하기
downloadLink.download = filename;

// 위에서 만든 blob과 링크를 연결
downloadLink.href = window.URL.createObjectURL(csvFile)

// 링크가 눈에 보일 필요는 없으니 숨겨줍시다.
downloadLink.style.display = "none"

// HTML 가장 아래 부분에 링크를 붙여줍시다.
document.body.appendChild(downloadLink)

// 클릭 이벤트를 발생시켜 실제로 브라우저가 '다운로드'하도록 만들어줍시다.
downloadLink.click()
}

function getCSV(filename) {
// csv를 담기 위한 빈 Array를 만듭시다.
var csv = []
var rows = document.querySelectorAll("#mytable table tr")

for (var i = 0; i < rows.length; i++) {
var row = [], cols = rows[i].querySelectorAll("td, th")

for (var j = 0; j < cols.length; j++)
row.push(cols[j].innerText)

csv.push(row.join(","))
}

// Download CSV
downloadCSV(csv.join("\n"), filename)
}

document.addEventListener('DOMContentLoaded', e => {
// CSV 버튼에 이벤트 등록
document.querySelector('#csvDownloadButton').addEventListener('click', e => {
e.preventDefault()
getCSV('mycsv.csv')
})
})

전체 예시

예제 html_to_csv.html에서 직접 동작하는 것을 확인해 보세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<!DOCTYPE html>
<html>
<head lang="ko">
<meta charset="utf-8">
<title>빈 HTML</title>
</head>
<body>
<table id="mytable">
<thead>
<tr>
<th>#</th>
<th>title</th>
<th>content</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Lorem Ipsum</td>
<td>로렘 입섬은 빈칸을 채우기 위한 문구입니다.</td>
</tr>
<tr>
<td>2</td>
<td>Hello World</td>
<td>헬로 월드는 언어를 배우기 시작할때 화면에 표준 출력을 할때 주로 사용하는 문구입니다.</td>
</tr>
</tbody>
</table>

<button id="csvDownloadButton">CSV 다운로드 받기</button>
</body>
<script type="text/javascript">
class ToCSV {
constructor() {
// CSV 버튼에 이벤트 등록
document.querySelector('#csvDownloadButton').addEventListener('click', e => {
e.preventDefault()
this.getCSV('mycsv.csv')
})
}

downloadCSV(csv, filename) {
let csvFile;
let downloadLink;

// CSV 파일을 위한 Blob 만들기
csvFile = new Blob([csv], {type: "text/csv"})

// Download link를 위한 a 엘리먼스 생성
downloadLink = document.createElement("a")

// 다운받을 csv 파일 이름 지정하기
downloadLink.download = filename;

// 위에서 만든 blob과 링크를 연결
downloadLink.href = window.URL.createObjectURL(csvFile)

// 링크가 눈에 보일 필요는 없으니 숨겨줍시다.
downloadLink.style.display = "none"

// HTML 가장 아래 부분에 링크를 붙여줍시다.
document.body.appendChild(downloadLink)

// 클릭 이벤트를 발생시켜 실제로 브라우저가 '다운로드'하도록 만들어줍시다.
downloadLink.click()
}

getCSV(filename) {
// csv를 담기 위한 빈 Array를 만듭시다.
const csv = []
const rows = document.querySelectorAll("#mytable tr")

for (let i = 0; i < rows.length; i++) {
const row = [], cols = rows[i].querySelectorAll("td, th")

for (let j = 0; j < cols.length; j++)
row.push(cols[j].innerText)

csv.push(row.join(","))
}

// Download CSV
this.downloadCSV(csv.join("\n"), filename)
}
}

document.addEventListener('DOMContentLoaded', e => {
new ToCSV()
})
</script>
</html>

하지만 이렇게하면 한글이 깨지는 문제가 있습니다.

한글 깨지는 문제 해결하기

앞서 한글이 깨지는 이유는 기본적으로 엑셀이 인코딩을 UTF-8로 인식하지 않기 때문에 문제가 발생합니다.

이때 아래 코드를 csv blob을 만들기 전 추가해주면 됩니다.

1
2
3
// 한글 처리를 해주기 위해 BOM 추가하기
const BOM = "\uFEFF";
csv = BOM + csv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// downloadCSV 함수를 이렇게 수정해 주세요.
downloadCSV(csv, filename) {
let csvFile;
let downloadLink;

// 한글 처리를 해주기 위해 BOM 추가하기
const BOM = "\uFEFF";
csv = BOM + csv

// CSV 파일을 위한 Blob 만들기
csvFile = new Blob([csv], {type: "text/csv"})

// Download link를 위한 a 엘리먼스 생성
downloadLink = document.createElement("a")

// 다운받을 csv 파일 이름 지정하기
downloadLink.download = filename;

// 위에서 만든 blob과 링크를 연결
downloadLink.href = window.URL.createObjectURL(csvFile)

// 링크가 눈에 보일 필요는 없으니 숨겨줍시다.
downloadLink.style.display = "none"

// HTML 가장 아래 부분에 링크를 붙여줍시다.
document.body.appendChild(downloadLink)

// 클릭 이벤트를 발생시켜 실제로 브라우저가 '다운로드'하도록 만들어줍시다.
downloadLink.click()
}

TreeShaking으로 webpack 번들 결과 용량 줄이기

이번 글은 webpack을 사용하고 있다고 가정합니다. 만약 webpack이 뭔지 아직 모르시거나 설치하지 않으셨다면 Webpack과 Babel로 최신 JavaScript 웹프론트 개발환경 만들기를 먼저 읽고 따라가보세요.

들어가며

웹 프론트 개발을 할 때 npm과 webpack을 통해 bundle.js와 같은 번들링된 js파일 하나로 만들어 싱글 페이지 앱을 만드는 경우가 많습니다.

우리가 사용하는 패키지들을 찾아 간단하게 묶고 babel을 통해 하위버전 브라우저에서도 돌아가도록 만들어주는 작업은 마치 마법과 같이 편리합니다.

하지만 이 마법같은 번들링에도 심각한 문제점이 있습니다. 바로 용량이 어마어마해진다는 것이죠.

아무런 처리를 하지 않고 webpack으로 빌드를 할 때의 용량은 스크린샷에 나온 것처럼 무려 1.61MB됩니다.

사실 아직 lodash, bootstrap3, axios와 같은 아주 기본적인 라이브러리들만 넣었음에도 다음과 같이 어마어마하게 무거운 js파일이 생성됩니다.

이제 이 파일을 1/3 크기로 줄여봅시다.

uglifyjs-webpack-plugin

webpack과 함께 파일의 용량을 줄여주는 도구인 uglifyjs를 사용해봅시다.

우선 다음 명령어로 uglifyjs-webpack-plugin를 설치해주세요.

1
npm install --save-dev uglifyjs-webpack-plugin

webpack 실행시 자동으로 용량줄이기

여러분이 webpack을 사용하고 있다면 아마 다음과 같은 webpack.config.js파일을 만들어 사용하고 있을거에요. (세부적인 설정은 다를 수 있어요.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const webpack = require('webpack');
const path = require('path');

module.exports = {
entry: './src/js/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
include: path.join(__dirname, 'src'),
exclude: /(node_modules)|(dist)/,
use: {
loader: 'babel-loader',
options: {
presets: [
["env"]
]
}
}
}
]
}
}

위 설정은 단순히 src파일 안의 js들을 dist폴더 안의 bundle.js파일로 묶어주고 있습니다.

이제 여기에서 몇줄만 추가해 주면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const webpack = require('webpack');
const path = require('path');
// 1. UglifyJSPlugin을 가져오세요.
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
entry: './src/js/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
include: path.join(__dirname, 'src'),
exclude: /(node_modules)|(dist)/,
use: {
loader: 'babel-loader',
options: {
presets: [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}
}
}
]
},
// 2. plugins를 새로 만들고, new UglifyJsPlugin() 을 통해
// UglifyJS를 빌드 과정에 합쳐주세요.
plugins: [
new UglifyJsPlugin()
]
}

이제 빌드를 실행해보면 아래 스크린샷과 같이 bundle.js파일의 용량이 획기적으로 줄어든 것을 볼 수 있습니다. 용량이 1.6MB에서 667KB로 1/3정도로 줄어든 것을 볼 수 있습니다. 간단하죠?

하지만 여기에는 작은 함정이 있습니다. 바로 time, 즉 빌드시마다 걸리는 시간도 그에따라 늘어난 것인데요, 만약 여러분이 webpack-dev-server와 같이 실시간으로 파일을 감시하며 변화 발생시마다 빌드하는 방식을 사용하고 있다면 코드 한줄, 띄어쓰기 하나 수정한 정도로 무려 12초에 달하는 빌드 시간을 기다려야 합니다. (treeshaking 하기 전에는 3초정도밖에 걸리지 않았습니다.)

그래서 항상 treeshaking을 해주는 대신 빌드작업, 즉 서버에 실제로 배포하기 위해 bundle.js파일을 생성할 때만 treeshaking을 해주면 개발도 빠르고 실제 배포시에도 빠르게 작업이 가능합니다.

빌드할때만 사용하기

앞서 다뤘던 package.json파일 중 script부분 아래 build를 다음과 같이 수정해주세요.

1
2
3
4
5
6
7
{
...
"scripts": {
"build": "webpack --optimize-minimize",
},
...
}

그리고 webpack.config.js 파일 중 위에서 넣어주었던 plugins를 통채로 지워주세요.(더이상 필요하지 않아요!)

만약 여러분이 webpack.config.js파일을 정확히 설정해 webpack이라는 명령어가 성공적으로 실행되고있던 상태라면 --optimize-minimize라는 명령어만 뒤에 붙여주면 곧바로 실행됩니다.

이제 여러분이 개발할 때 webpack-dev-server를 통해 빌드가 실행될때는 treeshaking이 되지 않고, 대신 배포를 위해 빌드를 할 때는 최소화된 작은 번들된 js파일을 가질 수 있게 됩니다.

마무리

여러분이 위 과정을 모두 따라왔다면 아마 package.jsonwebpack.config.js파일은 이와 유사하게 생겼을거에요.

1
2
3
4
5
6
7
8
9
// 앞뒤생략한 package.json
{
...
"scripts": {
"build": "webpack --optimize-minimize",
"devserver": "webpack-dev-server --open"
},
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// webpack.config.js 파일
const webpack = require('webpack');
const path = require('path');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
entry: './src/js/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
include: path.join(__dirname, 'src'),
exclude: /(node_modules)|(dist)/,
use: {
loader: 'babel-loader',
options: {
presets: [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}
}
}
]
}
// Plugin은 필요한 것만 넣어주세요. UglifyJSPlugin은 필요없어요!
}

Webpack과 Babel로 최신 JavaScript 웹프론트 개발환경 만들기

이번 포스팅에서는 nodejs8.5.0, npm5.3.0 버전을 사용합니다.

들어가며

파이썬의 버전 2와 3이 다른 것은 누구나 알고 2017년인 오늘은 대부분 Python3버전을 이용해 프로젝트를 진행합니다. 하지만 자바스크립트에 버전이 있고 새로운 기능이 나온다 하더라도 이 기능을 바로 사용하는 경우는 드뭅니다. 물론 node.js를 이용한다면 자바스크립트의 새로운 버전의 기능을 바로바로 이용해볼 수 있지만 프론트엔드 웹 개발을 할 경우 새로 만들어진 자바스크립트의 기능을 사용하는 것은 상당히 어렵습니다.

1
2
3
4
5
6
// 이런 문법은 사용하지 못합니다.
const hello = 'world'
const printHelloWorld = (e) => {
console.log(e)
}
printHelloWorld(hello)

가장 큰 차이는 실행 환경의 문제인데요, 우리가 자주 사용하는 크롬브라우저의 경우에는 자동업데이트 기능이 내장되어있어 일반 사용자가 크롬브라우저를 실행만 해도 최신 버전을 이용하지만, 인터넷 익스플로러나 사파리와 같은 경우에는 많은 사용자가 OS에 설치되어있던 버전 그대로를 이용합니다. 물론 이렇게 사용하는 것도 심각한 문제를 가져오지는 않지만, 구형 브라우저들은 새로운 자바스크립트를 이해하지 못하기 때문에 이 브라우저를 사용하는 사용자들은 새로운 자바스크립트로 개발된 웹 사이트를 접속할 경우 전혀 다르게 혹은 완전히 동작하지 않는 페이지를 볼 수 있기 때문에 많은 일반 사용자를 대상으로 하는 서비스의 경우 새 버전의 자바스크립트를 사용해 개발한다는 것이 상당히 모험적인 성향이 강합니다.

es2017

글쓴 시점인 2017년 10월 최신 자바스크립트 버전은 ES2017ES8이라 불리는 버전입니다. 하지만 이건 정말 최신 버전의 자바스크립트이고, 중요한 변화가 등장한 버전이 2015년도에 발표된 ES2015, 다른 말로는 ES6이라고 불리는 자바스크립트입니다. 하지만 인터넷익스플로러를 포함한 대부분의 브라우저들이 지원하는 자바스크립트의 버전은 ES5로 이보다 한단계 낮은 버전을 사용합니다. 따라서 우리는 ES6혹은 그 이상 버전의 자바스크립트 코드들을 ES5의 아래 버전 자바스크립트로 변환해 사용하는 방법을 사용할 수 있습니다.

Babel

여기서 바로 Babel이 등장합니다. Babel은 최신 자바스크립트를 ES5버전에서도 돌아갈 수 있도록 변환(Transpiling)해줍니다. 우리가 자바스크립트 최신 버전의 멋진 기능을 이용하는 동안, Babel이 다른 브라우저에서도 돌아갈 수 있도록 처리를 모두 해주는 것이죠!

물론, Babel이 마법의 요술도구처럼 모든 최신 기능을 변환해주지는 못합니다. 하지만 아래 사진처럼 다양한 브라우저에 따라 최신 JavaScript문법 중 어떠 부분까지가 실행 가능한 범위인지 알려줍니다. Babel coverage

Webpack

ES6에서 새로 등장한 것 중 유용한 문법이 바로 import .. from ..구문입니다. 다른 언어에서의 import와 유사하게 경로(상대경로 혹은 절대경로)에서 js파일을 불러오는 방식으로 동작합니다.

예를들어 어떤 폴더 안에 Profile.jsindex.js파일이 있다고 생각해 봅시다.

1
2
3
4
5
6
7
8
9
10
11
// Profile.js
export class Profile {
constructor(name, email) {
this.name = name
this.email = email
}

hello() {
return `Hello, ${this.name}(${this.email})`
}
}

하는일이라고는 name, email을 받는 것, 그리고 hello하는 함수밖에 없지만 우선 Profile이라는 class를 하나 만들었습니다.

여기서 Profile 클래스 앞에 export를 해 주었는데, export를 해 줘야 다른 파일에서 import가 가능합니다.

자, 아래와 같이 index.js파일을 하나 만들어 봅시다.

1
2
3
4
5
// index.js
import { Profile } from './Profile'

const pf = new Profile('Beomi', 'jun@beomi.net')
console.log(pf.hello())

이 파일은 현재 경로의 Profile.js파일 중 Profile 클래스를 import해와 새로운 인스턴스를 만들어 사용합니다.

하지만 안타깝게도 이 index.js파일은 실행되지 않습니다. 아직 webpack으로 처리를 해주지 않았기 때문이죠!

webpack-dev-server

webpack은 파일을 모아 하나의 js파일로 만들어줍니다.(보통 bundle.js라는 이름을 많이 씁니다.) 하지만 실제 개발중 js파일을 수정할 때마다 Webpack을 실행해 번들작업을 해준다면 시간도 많이 걸리고 매우 귀찮습니다. 이를 보완해 주는 패키지가 바로 webpack-dev-server 인데요, 이 패키지를 사용하면 여러분이 실제 빌드를 해 bundle.js파일을 만들지 않아도 메모리 상에 가상의 bundle.js파일을 만들어 여러분이 웹 사이트를 띄울때 자동으로 번들된 js파일을 띄워줍니다. 그리고 소스가 수정될 때 마다 업데이트된(번들링된) bundle.js파일로 띄워주고 화면도 새로고침해줍니다!

NOTE: webpack-dev-server는 build를 자동으로 해주는 것은 아닙니다. 단지 미리 지정해둔 경로로 접근할 경우 (실제로는 파일이 없지만) bundle.js파일이 있는 것처럼 파일을 보내주는 역할을 맡습니다. 개발이 끝나고 실제 서버에 배포할때는 이 패키지 대신 실제 webpack을 통해 빌드 작업을 거친 최종 결과물을 서버에 올려야 합니다.

설치하기

우선 npm프로젝트를 생성해야 합니다. index.js파일을 만든 곳(어떤 폴더) 안에서 다음 명령어로 “이 폴더는 npm프로젝트를 이용하는 프로젝트다” 라는걸 알려주세요.

1
2
# -y 인자를 붙이면 모든 설정이 기본값으로 됩니다.
npm init -y

이 명령어를 치면 폴더 안에 package.json파일이 생성되었을 거에요.

이제 다음 명령어로 Babel과 webpack등을 설치해 봅시다.

1
2
3
# babel과 webpack은 개발환경에서 필요하기 때문에 --save-dev로 사용합니다.
npm install --save-dev babel-loader babel-core babel-preset-env
npm install --save-dev webpack webpack-dev-server

babel-loaderwebpack이 .js 파일들에 대해 babel을 실행하도록 만들어주고, babel-core는 babel이 실제 동작하는 코드이고, babel-preset-env는 babel이 동작할 때 지원범위가 어느정도까지 되어야 하는지에 대해 지정하도록 만들어주는 패키지입니다.

이렇게 설치를 진행하고 나면 Babel과 Webpack을 사용할 준비를 마친셈입니다.

NOTE: package.json뿐 아니라 package-lock.json파일도 함께 생길수 있습니다. 이 파일은 npm패키지들이 각각 수많은 의존성을 가지고 있기 때문에 의존성 패키지들을 다운받는 URL을 미리 모아둬 다른 컴퓨터에서 package.json을 통해 npm install로 패키지들을 설치시 훨씬 빠른 속도로 패키지를 받을 수 있도록 도와줍니다.

이제 설정파일 몇개를 만들고 수정해줘야 해요.

설정파일 건드리기

package.json

package.json파일은 파이썬 pip의 requirements.txt처럼 패키지버전 관리만 해주는 것이 아니라 npm와 결합해 특정 명령어를 실행하거나 npm 프로젝트의 환경을 담는 파일입니다.

1
npm run 명령어이름

위와 같은 명령어를 사용할 수 있도록 만들어 주기도 합니다.

현재 package.json파일은 아래와 같은 형태로 되어있을거에요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "npm_blog",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.2"
}
}

이제 package.json파일을 열어 "scripts"부분을 다음과 같이 builddevserver명령어를 추가해 줍시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"name": "npm_blog",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"devserver": "webpack-dev-server --open --progress"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.1",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.2"
}
}

이제 여러분이 npm run build를 할 때는 webpack이 실행되고, npm run devserver를 할 때는 개발용 서버가 띄워질거에요.

webpack.config.js

webpack.config.js 파일은 앞서 설치해준 webpack을 실행 시 어떤 옵션을 사용할지 지정해주는 js파일입니다.

우리 프로젝트 폴더에는 아직 webpack.config.js 파일이 없을거에요. package.json와 같은 위치에 webpack.config.js파일을 새로 만들어 아래 내용으로 채워줍시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const webpack = require('webpack');
const path = require('path');

module.exports = {
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.js$/,
include: path.join(__dirname),
exclude: /(node_modules)|(dist)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env']
}
}
}
]
}
};

위 파일은 entry에 현재 위치의 index.js파일을 들어가 모든 import를 찾아오고, module -> rules -> include에 있는 .js로 된 모든 파일을 babel로 처리해줍니다.(exclue에 있는 부분인 node_modules폴더와 dist폴더는 제외합니다.)

index.html

사실 우리는 아직 번들링된 js파일을 보여줄 HTML파일이 없습니다! 우선 bundle.js를 보여주기만 할 단순한 HTML파일을 하나 만들어 봅시다.(index.js와 같은 위치)

1
2
3
4
5
6
7
8
9
10
11
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>NPM Webpack</title>
</head>
<body>
Webpack용 HTML
<script type="text/javascript" src="/dist/bundle.js"></script>
</body>
</html>

webpack을 사용하지 않았다면 HTML파일 아래 script태그의 src에 index.js를 넣어야 하지만, 우리는 webpack과 webpack-dev-server를 사용하기때문에 번들링된 파일의 위치인 /dist/bundle.js을 넣어줍니다.

devserver 띄우기

자, 이제 아래 명령어로 devserver를 띄워봅시다!

1
npm run devserver

브라우저의 개발자 도구를 열어보면 아래와 같이 로그가 잘 찍힌걸 확인해 볼 수 있을거에요.

이제 여러분이 index.js파일이나 Profile.js등을 수정하면 곧바로 새로고침되고 새로운 bundle.js를 라이브로 불러올거에요.

배포용으로 만들기

여러분이 프로젝트 개발을 끝내고 실제 서버에 배포할 때는 devserver가 아니라 실제로 번들링된 파일인 bundle.js를 만들어야 합니다.

아래 명령어로 현재 위치의 dist폴더 안에 bundle.js 파일을 만들어 줍시다.

1
npm run build

위와 같이 나온다면 성공적으로 webpack이 마쳐진 것이랍니다! 그리고 여러분 프로젝트 폴더 안에 dist폴더가 생기고 그 안에 bundle.js파일이 생겼을 거에요.

이제 여러분은 index.html파일과 dist폴더를 묶어 서버에 올리면 페이지가 잘 동작하는것을 확인할 수 있을거에요!

자바스크립트: function declaration와 Arrow Function의 this 스코프 차이

이번 포스팅은 ES6 JavaScript 대상입니다.

자바스크립트가 ES6로 개정되며 새로 들어온 것 중 Arrow Function이라는 것이 있습니다. () => {}의 모양을 갖고 있고 동작하는 것도 비슷하게 보입니다.

하지만 기존의 function() {} 함수형태를 1:1로 바로 변환할 수 있는 것은 아닙니다.

this, arguments의 바인딩이 다르다.

Arrow Functionthis 바인딩을 갖지 않습니다. 기존의 function에서 this의 탐색 범위가 함수의 {} 안에서 찾은 반면 Arrow Function에서 this는 일반적인 인자/변수와 동일하게 취급됩니다. 따라서 아래와 같은 상황이 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
// function(){}방식으로 호출할 때
function objFunction() {
console.log('Inside `objFunction`:', this.foo);
return {
foo: 25,
bar: function() {
console.log('Inside `bar`:', this.foo);
},
};
}

objFunction.call({foo: 13}).bar(); // objFunction의 `this`를 오버라이딩합니다.

위 결과는 아래와 같습니다.

1
2
Inside `objFunction`: 13 // 처음에 인자로 전달한 값을 받음
Inside `bar`: 25 // 자신이 있는 Object를 this로 인지해서 25를 반환

우리가 기대한 그대로 나옵니다.

하지만 Arrow Function을 실행하면 이야기가 약간 달라집니다.

1
2
3
4
5
6
7
8
9
10
// Arrow Function방식으로 호출할 때
function objFunction() {
console.log('Inside `objFunction`:', this.foo);
return {
foo: 25,
bar: () => console.log('Inside `bar`:', this.foo),
};
}

objFunction.call({foo: 13}).bar(); // objFunction의 `this`를 오버라이딩합니다.

위 코드의 결과는 아래와 같습니다.

1
2
Inside `objFunction`: 13 // 처음에 인자로 전달한 값을 받음
Inside `bar`: 13 // Arrow Function에서 this는 일반 인자로 전달되었기 때문에 이미 값이 13로 지정됩니다.

즉, Arrow Function 안의 thisobjFunctionthis가 됩니다.

그리고 이 ArrowFunction은 this의 Scope를 바꾸고 싶지 않을 때 특히 유용합니다.

1
2
3
4
5
6
7
8
9
10
// ES5 function에서는 `this` Scope가 function안에 들어가면 변하기 때문에 새로운 변수를 만들어 씁니다.
var someVar = this;
getData(function(data) {
someVar.data = data;
});

// ES6 Arrow Function에서는 `this` Scope의 변화가 없기 때문에 `this`를 그대로 사용하면 됩니다.
getData(data => {
this.data = data;
});

이와 같이 Arrow Function에서는 .bind method와 .call method를 사용할 수 없습니다.

즉, 비슷하게 보이지만 실제로 동작하는 것이 다르기 때문에 사용하는 때를 구별하는 것이 필요합니다.

Arrow Function은 new로 호출할 수 없다

ES6에서 함수는 callable한 것과 constructable한 것의 차이를 두고 있습니다.

만약 어떤 함수가 constructable하다면 new로 만들어야 합니다. 반면 함수가 callable하다면 일반적인 함수처럼 함수()식으로 호출하는 것이 가능합니다.

function newFunc() {}const newFunc = function() {}와 같은 방식으로 만든 함수는 callable하며 동시에 constructable합니다. 하지만 Arrow Function(() => {})은 callable하지만 constructable하지 않기때문에 호출만 가능합니다.

ps. ES6의 classconstructable하지만 callable하지 않습니다.

정리

함수 정의 방식을 바꿔서 사용할 수 있는 경우는 다음과 같습니다.

  • thisarguments를 사용하지 않는 경우
  • .bind(this)를 사용하는 경우

함수 정의 방식을 바꿔서 사용할 수 없는 경우는 다음과 같습니다.

  • new등을 사용하는 constructable한 함수
  • prototype에 덧붙여진 함수나 method들(보통 this를 사용합니다.)
  • arguments를 함수의 인자로 사용한 경우
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×