프로그래밍/Node.js

Node.js 3장 (2) 노드 내장 모듈

p-a-r-k 2019. 1. 4. 15:49
반응형

* 해당 글은 (주)길벗 `Node.js교과서` 내용을 바탕으로 복습 차 정리중입니다.


3.5 노드 내장 모듈 사용하기

노드는 웹브라우저의 자바스크립트보다 더 많은 기능을 제공합니다.

운영체제정보나 클라이언트가 요청한 주소에대한 정보 등을 가져올 수 있습니다.

노드에서 제공하는 기본모듈을 사용하면 됩니다.

3.5.1 os

웹브라우저에서는 운영체제의 정보를 가져올 수 없지만, 노드는 os모듈에 정보가 담겨있어서 가져올 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// os.js
const os = require('os')
 
console.log('운영체제 정보-------------')
console.log('os.arch(): ', os.arch())
console.log('os.platform(): ', os.platform())
console.log('os.type(): ', os.type())         // 운영체제 종류
console.log('os.uptime(): ', os.uptime())     // 운영체제 부팅 이후 흐른시간(초) [ process.uptime()은 노드의 실행시간 ]
console.log('os.hostname(): ', os.hostname()) // 컴퓨터의 이름
console.log('os.release(): ', os.release())   // 운영체제의 버전
 
console.log('경로--------------------')
console.log('os.homedir(): ', os.homedir())   // 홈 디렉터리 경로
console.log('os.timpdir(): ', os.timpdir())   // 임시파일 저장 경로
 
console.log('cpu 정보----------------')
console.log('os.cpus(): ', os.cpus())         // 컴퓨터의 코어정보
console.log('os.cpus().length: ', os.cpus().length)  // 코어갯수
 
console.log('메모리 정보--------------')
console.log('os.freemem(): ', os.freemem())   // 사용가능한 메모리(RAM)
console.log('os.totalmem(): ', os.totalmem()) // 전체 메모리 용량

process 객체와 겹치는 부분도 다소 있습니다. os모듈은 주로 컴퓨터 내부자원에 빈번하게 접근하는 경우 사용합니다.

즉, 일반적인 웹서비스 제작에는 빈도가 높지 않습니다. 운영체제별로 다른 서비스를 제공하고 싶을 때 유용할 것입니다.


3.5.2 path

폴더와 파일의 경로를 쉽게 조작하도록 도와주는 모듈입니다. 운영체제별로 경로 구분자가 다르므로 중요합니다.

windows: c\users\park 처럼 \로 구분합니다.

posix: /home/park 처럼 /로 구분합니다. (unix기반의 macOS와 linux)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// path.js
const path = require('path')
 
const string = __filename
 
console.log('path.sep: ', path.sep) // 경로 구분자 \, /
console.log('path.delimiter: ', path.delimiter) // 환경변수 구분자 ;, :
console.log('-----------------------')
console.log('path.dirname(): ', path.dirname(string)) // 파일이 위치한 폴더경로
console.log('path.extname(): ', path.extname(string)) // 파일의 확장자
console.log('path.basename(): ', path.basename(string)) // 파일의 이름
console.log('path.basename(): ', path.basename(string,
path.extname(string))) // 확장자를 두번째 인자로 넘길 시 이름만 표시
console.log('-----------------------')
console.log('path.parse(): ', path.parse(string)) // 경로를 5가지로 분리
// console.log('path.format(): ', path.format('parse한 객체')) // parse한 객체를 파일경로로 합침
console.log('path.normalize(): ', path.normalize('C://users\\\\jhPark\\\path.js')) // /나 \를 실수로 여러번 사용하거나 혼용할때 정상적인 경로로 변환
console.log('path.join(): ', path.join(__dirname, '..''..')) // 인자들을 하나의 경로로 결합
console.log('path.resolve(): ', path.resolve(__dirname, '..')) // join과 비슷하지만 /가 나오면 절대경로로 인식

3.5.3 url

주소를 쉽게 조작하도록 도와주는 모듈입니다. url처리는 크게 두가지 방식으로 나뉩니다.

노드 7버전에서 추가된 WHATWG(웹 표준을 정하는 단체의 이름) 방식과 예전부터 노드에서 사용하던 방식이 있습니다.

아래 이미지에서 가운데주소를 기준으로 위쪽이 기존노드의 방법, 아래쪽이 WHATWG의 방법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
// url.js
const url = require('url')
const URL = url.URL
const myURL = new URL(tmpString)
 
console.log('new URL(): ', myURL)
console.log('url.format(): ', url.format(myURL))
console.log('--------------------')
const parsedUrl = url.parse(tmpString)
console.log('url.parse(): ', parsedUrl)
console.log('url.format(): ', url.format(parsedUrl))

url 모듈 안에 URL생성자가 있습니다. 이 생성자에 주소를 넣어 '객체'로 만들면 주소가 부분별로 정리됩니다.

이방식이 WHATWG의 url입니다. WHATWG에만 있는 username, password, origin, searchParams 속성이 존재합니다.


기존노드 방식에서는 두 메서드를 주로 사용합니다.

url.parse(주소)

주소를 분해합니다. WHATWG방식과 비교하면 username과 password대신 auth속성이 있고, searchParams대신 query가 있습니다.

url.format(객체)

WHATWG방식의 url과 기존 노드의 url모두 사용할 수 있습니다. 분해되었던 url객체를 다시 원래 상태로 조립합니다.

WHATWG와 노드의 url은 취향에따라 사용하면 되지만, 노드의 url을 꼭 사용해야 하는 경우가 있습니다.

주소가 host없이 pathname만 오는경우 (/shop/index.asp) WHATWG방식은 처리할 수 없습니다.


위 콘솔결과에서 보이듯이 WHATWG방식은 search부분을 searchParams라는 객체로 반환하므로 유용합니다.

search부분은 보통 쿼리스트링으로 데이터를 전달할 때 사용됩니다.

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
// searchParams.js
const { URL } = require('url')
const getSearchParams = myURL.searchParams
 
console.log('searchParams: ', getSearchParams)
console.log('searchParams.getAll(): ', getSearchParams.getAll('type'))
console.log('searchParams.get(): ', getSearchParams.get('ANO'))
console.log('searchParams.has(): ', getSearchParams.has('ANO'))
 
console.log('searchParams.keys(): ', getSearchParams.keys())
console.log('searchParams.values(): ', getSearchParams.values())
 
getSearchParams.append('filter''es3')
getSearchParams.append('filter''es5')
console.log(getSearchParams.getAll('filter'))
 
getSearchParams.set('filter''es6')
console.log(getSearchParams.getAll('filter'))
 
getSearchParams.delete('filter')
console.log(getSearchParams.getAll('filter'))
 
console.log('searchParams.toString(): ', getSearchParams.toString())
myURL.search = getSearchParams.toString()

URL 생성자를 통해서 myURL이라는 주소 객체를 만들고, searchParams객체를 가져와서 다양한 메서드를 사용해보았습니다.

노드방식에서의 query 값 같은 문자열보다 searchParams가 유용한 이유는 querystring 모듈을 한번 더 사용해야하기 때문입니다.


3.5.4 querystring

WHATWG방식의 url대신 기존 노드의 url을 사용할 때 search부분을 사용하기 쉽게 객체로 만드는 모듈입니다.

1
2
3
4
5
6
7
8
// querystring.js
const url = require('url')
const qs = require('querystring')
 
const query = qs.parse(parsedUrl.query)
console.log('querystring.parse(): ', query)
console.log('querystring.stringify(): ', qs.stringify(query))

querystring.parse(쿼리)

url의 query부분을 자바스크립트 객체로 분해해줍니다.

querystring.stringify(객체)

분해된 query객체를 문자열로 다시 조립해줍니다.

3.5.5 crypto

다양한 방식의 암호화를 도와주는 모듈입니다. 몇 가지 메서드는 익혀두면 실제 서비스에도 적용할 수 있어서 유용합니다.

3.5.5.1 단방향 암호화

비밀번호는 보통 단방향 알고리즘을 사용하여 암호화합니다. 단방향 암호화란 복호화할 수 없는 암호화 방식을 뜻합니다.

복호화는 암호화 된 문자열을 원래 문자열로 되돌려 놓는 것을 의미합니다. 즉, 한번 암호화하면 원래 문자열을 찾을 수 없습니다.

비교할 때에는 암호화 된 db의 데이터와 비교할 정보를 db에 저장할때 사용한 알고리즘과 같은 알고리즘으로 암호화하여 비교합니다.

단방향 암호화 알고리즘은 주로 해시 기법을 사용합니다.

해시기법

문자열을 고정된 길이의 다른 문자열로 바꾸는 방식입니다. 예를 들어 parkjunghwan은 a78bas로 kimhuisu는 bsda8s로 바꾸는 겁니다.

입력문자열의 길이는 다르지만 출력문자열의 길이는 6자리로 고정되어있습니다. 노드에서는 아래와같이 사용합니다.

1
2
3
4
5
6
// hash.js
const crypto  = require('crypto')
 
console.log('base64: ', crypto.createHash('sha512').update('비밀번호').digest('base64'))
console.log('hex: ', crypto.createHash('sha512').update('비밀번호').digest('hex'))
console.log('base64: ', crypto.createHash('sha512').update('다른 비밀번호').digest('base64'))

'비밀번호' 라는 문자열을 해시를 사용하여 바꾸었습니다.

createHash(알고리즘): 사용할 알고리즘을 넣어줍니다. md5, sha1, sha256, sha512등이 가능하지만 md5와 sha1은 취약점이 발견되었습니다.

현재는 sha512정도로 충분하지만 나중에 취약해지면 더 강화된 알고리즘으로 바꿔야 합니다.

update(문자열): 변환할 문자열을 넣어줍니다.

digest(인코딩): 인코딩할 알고리즘을 넣어줍니다. base64, hex, latin1이 주로 사용되는데, 그중 base64가 짧아 애용됩니다.


현재는 주로 pbkdf2나 bcrypt, scrypt라는 알고리즘으로 암호화하고 있습니다.

이중에 노드에서 지원하는 pbkdf2에 대해 알아보겠습니다.

간단히 설명하면, 기존문자열에 salt라고 불리는 문자열을 붙인 후 알고리즘을 반복해 적용하는 것입니다.

1
2
3
4
5
6
7
8
9
10
// pbkdf2.js
const crypto = require('crypto')
 
crypto.randomBytes(64, (err, buf) => {
    const salt = buf.toString('base64')
  console.log('salt: ', salt)
  crypto.pbkdf2('비밀번호', salt, 100000, 64, 'sha512', (err, key) => {
    console.log('password: ', key.toString('base64'))
  })
})

randomBytes() 메서드로 64바이트 길이의 문자열을 만드는데 이것이 salt가 됩니다.

pbkdf2() 메서드에는 비밀번호, salt, 반복수, 출력바이트, 알고리즘 을 인자로 넣어줍니다.


3.5.5.2 양방향 암호화

양방향 대칭형 암호화는 암호화된 문자열을 복호화 할 수 있습니다. 라는 것이 사용됩니다.

암호를 복호화 하려면 암호화할 때 사용한 키와 같은 키를 사용해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
// cipher.js
const crypto = require('crypto')
 
const cipher = crypto.createCipher('aes-256-cbc''열쇠')
let result = cipher.update('암호화할 문장''utf8''base64')
result += cipher.final('base64')
console.log('암호화: ', result)
 
const decipher = crypto.createDecipher('aes-256-cbc''열쇠')
let result2 = decipher.update(result, 'base64''utf8')
result2 += decipher.final('utf8')
console.log('복호화: ', result2)

원래 문장으로 복호화 되었습니다.

이외에도 crypto모듈을 양방향 비대칭형 암호화, HMAC등 암호화를 제공하고 있으니 확인해보면 좋습니다.


3.5.6 util

util 이라는 이름처럼 각종 편의 기능을 모아둔 모듈이며, 추가되고 deprecated되어 사라지는 경우도 있습니다.

deprecated?

프로그래밍 용어로, '중요도가 떨어져 더 이상 사용되지 않고 앞으로는 사라지게 될'것 이라는 뜻입니다.

새로운 기능이 나와서 기존보다 좋을 때, 기존기능을 deprecated 처리하곤 합니다.

이전 사용자를 위해 기능을 제거하지는 않지만 곧 없앨 예정이므로 더 이상 사용하지 말라는 의미입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const util = require('util')
const crypto = require('crypto')
 
const dontUseMe = util.deprecate((x, y) => {
   console.log(x+y)
}, 'dontUseMe 함수는 deprecated되었으니 더 이상 사용하지마셈..')
dontUseMe(1, 2)
 
const randomBytesPromise = util.promisify(crypto.randomBytes)
randomBytesPromise(64)
.then((buf) => {
   console.log(buf.toString('base64'))
})
.catch((err) => {
   console.error(err)
})


3.6 파일 시스템 접근

fs모듈은 파일시스템에 접근하는 모듈입니다. 즉, 파일을 생성하거나 삭제하고, 읽거나 쓸 수 있습니다. 폴더도 만들었다 지웠다 할 수 있습니다. 

1
2
// readme.txt
저를 읽어보세요

fs 모듈을 불러온 뒤 읽을 파일의 경로를 지정합니다. 유의할 점은 콜백 함수도 readFile의 인자로 같이 넣어줘야 합니다.

이 콜백함수의 매개변수로 에러와 데이터를 받습니다.

1
2
3
4
5
6
7
8
9
10
const fs = require('fs')
 
fs.readFile('./readme.txt', (err, data) => {
   if (err) {
      throw err;
   }
 
   console.log(data);
   console.log(data.toString());
})

받은 data를 콘솔로 찍으니 Buffer형식이 찍히지만 toString으로 변환하니 제대로 문자열이 출력됩니다.

readFile은 버퍼형식으로 제공됩니다. 버퍼는 메모리의 데이터라고 생각하면 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fs = require('fs')
 
fs.writeFile('./writeme.txt''글이 입력됨 ㅋ', (err) => {
   if (err) {
      throw err;
   }
 
   fs.readFile('./writeme.txt', (err, data) => {
      if (err) {
         throw err;
      }
 
      console.log(data.toString())
   })
})

파일이 만들어지고, 파일읽기도 성공했습니다. 


3.6.1 동기메서드와 비동기 메서드

setTimeout이나 process.nextTick 외에도 노드는 대부분의 메서드를 비동기식으로 처리합니다. 하지만 몇몇 메서드는 동기방식으로도 동작합니다.

fs모듈은 동기방식의 메서드를 많이 가지고 있습니다. fs.readFile메서드를 순서대로 여러번 쓰면 콘솔이 찍히는 순서가 매번 다를 것 입니다.

이유는 비동기 방식이기 때문에 읽으라고만 요청하고 다음작업으로 넘어갑니다.

readFile()로 인해서 백그라운드에서 파일을 읽고 읽은 순서대로 메인스레드에 각 순서별로 콜백함수를 보내 실행합니다.

이 방식은 상당히 좋습니다. 수백개의 I/O요청이 들어와도 메인스레드는 백그라운드에 처리를 위임하고, 그후로도 요청을 더 받을 수 있습니다.

나중에 백그라운드가 각각의 처리가 완료되었다고 알리면 그때 콜백함수를 처리합니다.

동기와 비동기, 블로킹과 논블로킹

노드에서는 동기와 비동기, 블로킹과 논블로킹이라는 네 용어가 혼용됩니다. 용어가 다른만큼 의미차이가 있습니다.

  • 동기와 비동기 : 함수가 바로 return되는지 여부
  • 블로킹과 논블로킹 : 백그라운드 작업 완료 여부

노드에서는 동기-블로킹, 비동기-논블로킹 방식이 대부분입니다. 그외엔 없다고 봐도됩니다.

동기-블로킹 방식에서는 백그라운드 완료여부를 계속 확인하며, 함수가 바로 return되지 않고 백그라운드 작업이 끝나야 return 됩니다.

비동기-논블로킹 방식에서는 호출한 함수가 바로 return되어 다음 작업을 넘어가고, 나중에 백그라운드가 알림을주면 처리합니다.(콜백함수 실행)


동기방식의 readFileSync메서드를 테스트해봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs')
 
console.log('시작')
 
let data = fs.readFileSync('./readme2.txt')
console.log('1번', data.toString())
 
data = fs.readFileSync("./readme2.txt");
console.log("2번", data.toString());
 
data = fs.readFileSync("./readme2.txt");
console.log("3번", data.toString());
 
console.log('끝ㅋ')

콜백함수를 넣지않고 변수로 받아서 아랫줄에서 바로 사용하고 있습니다. 

이런식의 동기방식은 수백개 이상 요청에서는 성능에 문제가 생깁니다. 동기메서드들은 이름 뒤에 Sync가 붙어있어서 구분하기 쉽습니다.

writeFileSync도 있습니다. 동기메서드를 사용해야하는 경우는 극히 드뭅니다. 

비동기 방식인 readFile에서도 콜백함수 내부에 중첩하여 readFile을 써주면 동기처럼 순서대로 찍히겠죠.

하지만, 그러면 콜백지옥이 펼처지니 promise나 async/await로 어느정도 해결해야합니다.


3.6.2 버퍼와 스트림 이해하기

readFile() 메서드에서 버퍼형식인 data를 toString()으로 변환하는 이유를 알아볼 차례입니다.

파일을 읽거나 쓰는 방식에는 크게 두 가지 방식, 즉 버퍼와 스트림을 이용하는 방식이 있습니다.

버퍼링과 스트리밍이라는 용어를 들어본적이 있으실텐데, 영상을 로딩중일때 버퍼링, 실시간 송출할때 스트리밍 한다고합니다.

버퍼링은 영상을 재생할 수 있을 때까지 데이터를 모으는 동작, 스트리밍은 방송인의 컴퓨터에서 시청자 컴퓨터로 영상데이터를 조금씩 전송하는 동작입니다.

노드의 버퍼와 스트림도 비슷한 개념입니다.

노드는 readFile() 메서드 처럼 파일을 읽을 때 메모리에 파일 크기만큼 공간을 마련해두고, 파일데이터를 메모리에 저장한 뒤 사용자가 조작하도록 해줍니다.

메모리에 저장된 데이터가 바로 버퍼입니다.


문제는 용량이 100MB인 파일이 있으면 메모리에 100MB인 버퍼를 만들어야하는데, 동시에 열개만해도 1G메모리가 필요합니다.

그래서 버퍼크기를 작게 만들어서 여러번에 나눠서 보내는 방식이 등장했는데, 

버퍼 1MB를 만든후 100MB파일을 백번에 걸쳐 보내는 것입니다. 메모리 1MB로 100MB파일을 전송할 수 있습니다. 이것이 스트림입니다.


파일을 읽는 스트림 메서드로는 createReadStream이 있습니다.

// readme3.txt
저는 조금 조금 나눠서 전달됨. 나눠진 조각을 chunk라고 부름니다

createReadStream() 으로 읽기 스트림을 만들어줍니다. 파일경로와, 옵션을 인자로 넣습니다.

highWaterMark 옵션은 버퍼의 크기(바이트 단위)를 정할 수 있는 옵션입니다. 기본값은 64kb입니다.

readStream은 이벤트 리스너를 붙여서 사용합니다. 보통 data, end, error 이벤트를 사용합니다.

파일을 읽는도중 에러가 발생하면 .on("error") 이벤트가 호출되고, 읽기가 시작되면 .on("data") 이벤트가 호출됩니다.

16b씩 읽도록 설정했으므로 파일의 크기가 16b다 크면 여러번 실행될 겁니다. 끝나면 .on("end")가 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// createReadStream.js
const fs = require('fs')
 
const readStream = fs.createReadStream('./readme3.txt', {highWaterMark: 16})
const data = []
 
readStream.on('data', (chunk) => {
   data.push(chunk)
   console.log(`data: ${chunk}, ${chunk.length}`)
})
 
readStream.on("end", () => {
   console.log("end", Buffer.concat(data).toString())
})
 
readStream.on("error", (err) => {
   console.log("error: ", err)
})

파일을 쓸수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
// createWriteStream.js
const fs = require('fs')
 
const writeStream = fs.createWriteStream('./writeme2.txt')
writeStream.on("finish", () => {
   console.log('파일쓰기 완룡')
})
 
writeStream.write('이글을 씁니당..\n')
writeStream.write('한번 더 썹니다..')
writeStream.end()

실행히보면 writeme2.txt에 write()한 문자열들이 들어있을 겁니다.


createReadStream으로 파일을 읽고 스트림을 전달받아 createWriteStream으로 파일을 쓸수도 있습니다. 파일 복사와 비슷합니다.

스트림 끼리 연결하는 것을 파이핑한다 라고 표현합니다. 액체가 흐르는 관(파이프) 처럼 데이터가 흐른다고해서 지어진 이름입니다.

노드 8.5 버전이 나오기 전까지는 두 스트림을 pipe하여 파일을 복사하곤 했지만 새로운 파일 복사 방식이 나왔습니다. (3.6.3절)



3.6.3 기타 fs메서드

fs는 파일 시스템을 조작하는 다양한 메서드를 제공합니다. 위에서는 단순히 읽기/쓰기를 했지만 생성/삭제, 폴더 생성/삭제도 있습니다.

fs.access(경로, 옵션, 콜백) : 폴더나 파일에 접근할 수 있는지를 체크합니다.

fs.mkdir(경로, 콜백) : 폴더를 만드는 메서드입니다. 이미 폴더가 있다면 에러가 발생하므로 access() 메서드로 확인하는게 좋습니다.

fs.open(경로, 옵션, 콜백) : 파일의 아이디를 가져오는 메서드입니다. 파일이 없다면 생성 후 가져옵니다.

아이디로 fs.read() 나 fs.write() 메서드로 읽거나 쓸 수 있습니다.

fs.rename(기존 겨올, 새 경로, 콜백) : 파일의 이름을 바꾸는 메서드입니다. 기존파일위치와 새로운 파일 위치를 적어주면 됩니다.


fs.readdir(경로, 콜백) : 폴더 안의 내용물을 확인할 수 있습니다. 

fs.unlink(경로, 콜백) : 파일을 지울 수 있습니다. 파일이 없다면 에러가 발생하므로 먼저 있는지 확인해야합니다.

fs.rmdir(경로, 콜백) : 폴더를 지울 수 있습니다. 폴더 안에 파일이 있다면 에러가 발생하므로 내부파일을 모두지우고 호출해야 합니다.


노드 8.5버전에서 추가된 파일 복사 방법이 있습니다. createReadStream과 writeStream을 pipe하지 않아도 됩니다.

1
2
3
4
5
6
7
8
9
10
// copyFile.js
const fs = require('fs')
 
fs.copyFile('./readme4.txt''./writeme4.txt', (err) => {
   if (err) {
      return console.error(err)
   }
 
   console.log('복사완료')
})

readme4.txt를 만들고 실행시키면 writeme4.txt가 생성 되었을 겁니다.



3.7 이벤트 이해하기

스트림을 배울 때 on('data', 콜백) 또는 on('end', 콜백)을 사용했습니다. 

바로 data와 end라는 이벤트가 발생할 때 콜백함수를 호출해라...고 이벤트를 등록한 것입니다. 이벤트는 직접 만들 수 있습니다.

 원본 펼치기

events모듈을 사용하면 됩니다. myEvent라는 객체를 만들면 이벤트 관리를 위한 메서드를 사용할 수 있습니다.

이제 아직까지 배운 개념들로도 서버를 만들 수 있습니다. 하지만 서버 운영 시 코드에 에러가 발생하면 치명적이므로,

마지막으로 에러를 처리하는 방법에 대하여 살펴보겠습니다.



3.8 예외 처리하기

노드에서는 예외처리가 정말 중요합니다. 예외란 처리하지 못한 에러를 가리키며, 이러한 예외들은 노드 프로세스를 멈추게 만듭니다.

멀티스레드에서는 스레드 하나가 멈추면 다른 스레드가 대신 할 수 있습니다. 하지만 노드는 싱글스레드이므로 하나를 소중히 해야합니다.

따라서 에러를 처리하는 방법을 익혀두어야 합니다. 에러로그는 기록하되 작업은 계속 진행될 수 있도록 말입니다.


아래 코드로 에러를 잡아봅니다. 발생할 것 같은 부분을 try catch로 감싸줍니다.

1
2
3
4
5
6
7
8
9
// error1.js
setInterval(() => {
   console.log('시작')
   try {
      throw new Error('서버를 고장내주마!')
   catch (e) {
      console.error(e)
   }
}, 1000)

setInterval은 프로세스가 멈추는지 체크하기 위해서입니다. 프로세스가 멈추면 setInterval도 멈출것 입니다.

내부에 throw new Error()로 에러를 강제로 발생시켰습니다.

에러는 발생하지만 try catch로 잡을 수 있고 setInterval도 직접 멈추기 전까지 실행됩니다.

이렇게 에러가 발생할 것 같은 부분을 미리 try catch로 감싸면 됩니다.


위에서 fs등 코드에서 에러가 발생할경우 throw err; 를 해주었는데. throw를 하면 노드가 멈춥니다.

따라서 throw의 경우에는 try cath로 잡아줘야합니다.


노드 내장모듈의 에러

노드 내장모듈들의 에러는 프로세스를 멈추지 않습니다. 로그는 기록하고 나중에 원인을 찾아 수정하면 됩니다.


마지막으로 예측이 불가능한 에러를 처리하는 방법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
// error3.js
process.on('uncaughtException', (e) => {
   console.error('예기치 못한 에러', e)
})
 
setInterval(() => {
   throw new Error('고장내주마')
}, 1000)
 
setTimeout(() => {
   console.log('실행됩니다.')
}, 2000)

process 객체에 uncaughtException 이벤트 리스너를 달아주었습니다. 처리하지못한 에러가 발생하면 이부분이 실행되고 프로세스도 유지됩니다. 

이부분이 없다면 위예제 에서는 setTimeout이 실행되지 않습니다.


uncaughtException 으로 모든 에러를 처리할 수 있을 것 같이 보이지만, 노드 공식문서에서는 uncaughtException 이벤트를 최후의 수단으로 사용하라고 말하고 있습니다.

노드는 uncaughtException 이벤트 발생 후 다음 동작이 제대로 동작하는지 보증하지 않습니다. 즉, 복구작업 코드를 넣었더라도 동작을 확신할 수 없습니다.

따라서 에러내용을 기록하는정도로 사용하고 process.exit() 로 프로세스를 종료하는것이 좋습니다.


프로세스를 종료하면 서버가 멈추어서 걱정되겠지만, 운영 중인 서버에서 프로세스가 종료되었을 때 재시작하는 방법을 나중에 배웁니다.

서버운영은 에러와의 싸움입니다. 모든상황에 대비하는것이 최선이지만 미처 발견하지 못한 에러가 있을 수 있습니다.

따라서 에러 발생시 철저히 기록(로깅) 하는 습관을 들이고, 주기적으로 로그를 확인하며 보완해야합니다.


아직까지 기본적인 내용은 충분히 배웠고, 웹 서버를 실제 만들어보며 배운 내용들을 적용해봅니다.


반응형