JUINTINATION
Node.js와 Express.js 가볍게 입문해보기 - 1 본문
지난 도커(Docker) 가볍게 입문해보기 글에서 도커의 가벼운 입문 말고 Express.js 기본적인 CURD API 만들어보기, Middleware에 대한 개념 이해하기 등의 과제를 받았다고 언급했었다. 이 글에서는 Node.js에 관련된 내용을 적을 것이다.
나는 자바 스프링 프레임워크로 웹 애플리케이션을 개발해본 경험이 있기 때문에 ETRI에서 실무에 바로 적용하는 Node.js라는 책을 대여해서 구글링과 함께 참고하여 공부했다. 일단은 Node.js 작동 원리와 같은 개념적인 내용은 최대한 패스하고 바로 코드적인 내용으로 넘어가도록 하겠다.
Node.js 설치
나는 Mac OS 환경에서 홈브루(Homebrew)를 사용하여 설치하였다. 명령어는 다음과 같다.
$ brew install node
$ brew install npm
또한 node를 설치하는 과정에서 mysql과 php가 이미 설치되어 있었기 때문에 다음과 같은 명령어를 추가로 입력했다.
$ brew services start mysql
$ brew services start php
이후에 설치 결과를 확인하기 위해 다음 명령어를 입력한다.
$ node -v
$ npm -v
현재 node 버전은 21.5.0, npm 버전은 10.2.4이다. 콘솔을 시작하려면 터미널에서 $ node
를 입력하면 끝이다. $ .exit
를 입력하면 종료할 수 있다.
위와 같이 정상적으로 실행되는 것을 확인할 수 있다.
Node.js 기본 문법
자바스크립트에서 주로 사용하는 명명 규칙은 CamelCase이다. 클래스명의 경우에는 CapitalCamelCase를 사용한다.
- 올바른 예시
- camelCase(일반적인 변수 이름)
- isCamelCase(Boolean 타입의 변수 이름)
- 잘못된 예시
- camel_case(스네이크 표기법 형태로 지어진 변수명)
Node.js는 원시(primitive) 타입의 수가 적다. 문자열, 숫자(정수 및 실수), 불리언, Undefined, Null, RegExp 외의 데이터들은 모두 객체 타입이다. 즉, 변경 가능한 형식에 맞춰진 컬렉션이며 자동 형변환은 대부분 제대로 동작한다.
또한 자바 스크립트에는 다음과 같이 원시 타입을 위한 헬퍼(helper)를 포함하고 있는 String, Number, Boolean 객체도 존재한다.
'a' === new String('a') // false
'a' === new String('a').toString() // true
'a' == new String('a') // true
==는 자동 형변환이 일어나지만 ===는 그렇지 않다는 것을 확인할 수 있다
또한 Node.js는 버퍼라는 특별한 데이터 타입이 있다. 4개의 원시 데이터 타입(불리언, 문자열, 숫자, RegExp)과 프론트엔드 자바스크립트의 모든 객체 데이터타입에서 추가된 Node.js 데이터 타입으로 데이터를 굉장히 효율적으로 저장한다.
객체 표기는 다음과 같이 작성할 수 있다.
var obj = {
color: "green",
type: "suv",
owner: {
...
}
}
함수 또한 객체이며 다음과 같이 작성한다.
var obj = function () {
this.color = "green",
this.type = "suv",
this.owner = {
...
}
}
함수를 정의 또는 생성하기 위해 가장 흔히 사용되는 세 가지 방법인 선언적 표현, 변수에 할당하는 익명 표현, 또는 두 방법 모두 사용하는 표현이 있다. 다음은 선언적 함수의 예다.
function f () {
console.log('Hi');
return true;
}
변수에 할당하는 익명 함수는 다음과 같다. 앞의 예제와 달리 변수가 생성되기 전까지는 함수를 사용할 수 없다.
var f = function () {
console.log('Hi');
return true;
}
다음은 앞의 두 방법을 함께 사용한 예다.
var f = function f () {
console.log('Hi');
return true;
}
프로퍼티를 가진 함수는 다음과 같다.
var f = function () {console.log('Boo');}
f.boo = 1;
f(); // 'Boo' 출력
console.log(f.boo); // 1 출력
return 키워드는 생략할 수 있으며 이 경우는 함수가 호출될 때 undefined를 반환한다.
또한 함수를 객체처럼 취급하기 때문에 함수를 다른 함수의 매개변수로 전달할 수도 있으며 보통 Node.js의 콜백 함수를 전달한다.
var convertNum = function (num) {
return num + 10;
}
var processNum = function (num, fn) {
return fn(num)
}
processNum(10, convertNum);
배열은 Array.prototype 전역 객체로부터 상속받은 일부 특별한 메서드를 가진 실제 배열은 아닌, 고유의 정수 키 값을 가진 객체이다.
var arr = [];
var arr2 = [1, "Hi", {a:2}, function () {console.log('boo');}];
var arr3 = new Array();
var arr2 = new Array(1, "Hi", {a:2}, function () {console.log('boo');});
자바 스크립트는 객체들이 다른 객체로부터 바로 상속받기 때문에 클래스가 존재하지 않으며 이런 상속을 프로토타입 기반 상속이라 부른다. 대신 몇 가지 상속 패턴 타입이 존재한다.
- 클래스적 상속
- 가짜 클래스(pseudoclassical)적 상속
- 함수적 상속
함수적 상속 패턴의 예제는 다음과 같다.
var user = function (ops) {
return { firstName: ops.name || 'John'
, lastName: ops.name || 'Doe'
, email: ops.email || 'test@test.com'
, name: function() { return this.firstName + this.lastName}
}
}
var agency function(ops) {
ops = ops || {}
var agency = user(ops)
agency.customers = ops.customers || 0
agency.isAgency = ture
return agency
}
Node.js 전역 변수와 예약 키워드
동일한 모델을 기준으로 설계되었음에도 불구하고, Node.js와 브라우저 자바 스크립트의 전역 변수는 서로 다르다. var이 생략되었을 때, 브라우저 자바스크립트는 전역 공간에 변수를 보내면서 전역 공간을 더럽히므로 Node.js에서는 일부러 다르게 설계하였다. 이런 부분은 자바스크립트의 문제점 중 하나로 언급되기도 한다.
브라우저 자바스크립트에는 window 객체가 존재한다. 하지만 Node.js의 경우 window 객체가 없는 대신 새로운 객체 또는 키워드가 존재한다.
- process
- global
- module.exports와 exports
브라우저 자바스크립트는 기본적으로 전역 범위에 모든 것을 저장한다. 반면, Node.js는 기본적으로 로컬 범위에 저장한다. 전역 범위에 접근해야 하는 경우 global 객체를 사용하며 내보내야 하는 경우에는 명시적으로 수행한다.
Node.js에서 객체를 내보내기 위해 exports.name = object;를 사용하며 예는 다음과 같다.
// messages.js 파일(경로와 파일명을 routes/messages.js라고 가정한다.
var messages = {
find: function(req, res, next) {
...
},
add: function(req, res, next) {
...
},
format: 'title | date | author'
}
exports.messages = messages;
// 다른 파일 (main.js 등)
var importedObject = require('./routes/messages.js');
그러나 가끔 생성자를 호출하는 것이 좀 더 나은 경우가 있는데 예를 들자면 Express.js 애플리케이션에 프로퍼티를 추가할 때와 같은 경우다. 이 경우에는 module.exports가 필요하다.
// 샘플 모듈
module.exports = function(app) {
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
return app;
}
이 샘플 모듈을 포함하는 파일에 다음 내용을 작성한다.
...
var app = express();
var config = require('./config/index.js');
app = config(app);
...
좀 더 간결한 코드는 var = express(); require('./config/index.js')(app);
이다. 핵심 Node.js 모듈의 경우 require('name')
과 같이 경로 없이 파일명만 사용한다.
__dirname vs process.cwd
__dirname은 전역 변수가 호출된 파일의 절대 경로인 반면, process.cwd는 스크립트를 실행하는 프로세스의 절대 경로다. $ node ./code/program.js
커맨드를 실행시키는 것처럼 프로그램을 다른 폴더에서 실행시킨다면 __dirname과 process.cwd는 다를 수 있다.
브라우저 API 헬퍼
Node.js에는 브라우저 자바스크립트 API의 무수히 많은 헬퍼 함수들이 존재한다. 가장 유영한 함수는 String, Array, 그리고 Math 객체의 함수들이다. 다음은 가장 흔히 사용되는 함수들의 목록과 역할들이다.
- Array
- some(), every(): 배열 요소 삽입
- join(), concat(): 문자열로 변환
- pop(), push(), shift(), unshift(): 스택과 큐로 작업
- map(): 배열 요소 맵핑
- filter(): 배열 요소에 대한 쿼리(query) 수행
- sort(): 요소 정렬
- reduce(), reduceRight(): 연산
- slice(): 배열 추출
- splice(): 삭제
- indexOf(): 배열 내 해당 값이 위치한 인덱스
- reverse(): 역순으로 나열
- in 연산자: 배열 요소에서의 반복문
- Math
- random(): 1보다 작은 실수인 난수
- String
- substr(), substring(): 부분 문자열 추출
- length: 문자열의 길이
- indexOf(): 문자열에서 값을 찾기 위한 인덱스
- split(): 문자열을 배열로 변환
뿐만 아니라 특정한 시간 간격(delay 밀리초)으로 주기적으로 콜백 함수를 실행하는 setInterval(), 특정 시간(delay 밀리초)이 경과한 후에 한 번만 콜백 함수를 실행하는 setTimeout(), 배열의 각 요소에 대해 주어진 콜백 함수를 실행하는 forEach(), 그리고 콘솔 메서드가 있다.
setInterval(function() {
console.log("Hello, World!");
}, 1000);
setTimeout(function() {
console.log("Hello, World!");
}, 2000);
const numbers = [1, 2, 3, 4, 5];
numbers.forEach(function(number, index) {
console.log(`Index ${index}: ${number}`);
});
- 몇 가지 일반적인 콘솔 메서드:
- console.log(): 메시지를 출력합니다.
- console.warn(): 경고 메시지를 출력합니다.
- console.error(): 오류 메시지를 출력합니다.
- console.info(): 정보성 메시지를 출력합니다.
Node.js 핵심 모듈
Node.js는 다른 프로그래밍 기술과 달리 많은 표준 라이브러리를 제공하지 않는다. Node.js의 핵심 모듈은 가장 기본적인 것만 있으며 나머지 모듈들은 NPM 레지스트리를 통해 선택할 수 있다. 주요 핵심 모듈, 클래스, 메서드, 그리고 이벤트는 다음과 같다.
- http: Node.js HTTP 서버를 위한 기본 모듈
- http.createServer(): 새로 생성된 웹 서버 객체 반환
- http.listen(): 명시된 포트와 호스트명의 접속 허용(서버 실행)
- http.createClient(): 클라이언트로 다른 서버에 요청
- http.serverRequest(): 수신된 요청을 요청 핸들러에 전달
- data: 메시지 바디 부분을 수신할 때 보낸다.
- end: 각각의 요청마다 한 번만 보낸다.
- request.method(): 문자열로 된 요청 메서드
- request.url(): 요청 URL 문자열
- http.ServerResponse(): 유저가 아닌 HTTP 서버가 내부적으로 해당 객체 생성, 요청 핸들러의 출력으로 사용됨
- response.writeHead(): 요청에 응답 헤더를 보낸다.
- response.write(): 응답 바디를 보낸다.
- rewponse.end(): 응답 바디를 보내고 종료한다.
- util: 디버깅을 위한 유틸리티 제공
- util.inspect(): 객체의 정보를 문자열로 반환하며 디버깅하는 데 유용하다.
- querystring: 쿼리 문자열을 처리하기 위한 유틸리티 제공
- querystring.stringify(): 객체를 쿼리 문자열 형태로 직렬화(seralize)
- querystring.parse(): 쿼리 문자열을 객체 형태로 객체화
- url: URL 분석과 파싱을 위한 유틸리티 제공
- parse(): URL 문자열을 객체 형태로 반환
- fs: 파일 읽기와 쓰기 같은 파일 시스템 작업 처리
- fs.readFile(): 비동기식으로 파일을 읽는다.
- fs.writeFile(): 비동기식으로 파일에 데이터를 쓴다.
핵심 모듈은 설치하거나 다운로드할 필요가 없다. 애플리케이션에 모듈을 포함시키려면 다음 문법을 사용하면 된다.
var http = require('http');
그 외 모듈은 다음 위치에 존재한다.
- npmjs.org: NPM 레지스트리
- GitHub: Joyent가 관리하는 Node.js 모듈
- nodetoolbox.com: 통계 기반의 레지스트리
- Nipster: Node.js를 위한 NPM 검색 도구
- Node 트래킹: GitHub 통계 기반의 레지스트리
유용한 Node.js 유틸리티
- Crypto: 랜더마이저(randomizer), MD5, HMAC-SHA1, 그리고 다른 알고리즘을 제공
- Path: 시스템 경로 처리
- String decoder: 버퍼를 문자열 타입으로 변환하거나 문자열을 버퍼로 변환
Node.js의 데이터 스트리밍
데이터 스트리밍이란 애플리케이션이 여전히 데이터를 수신하고 있는 동안에도 그 데이터를 처리하는 것을 의미한다. 이 기능은 비디오나 데이터베이스 마이그레이션과 같이 크기가 굉장히 큰 데이터 셋을 처리하는 데 유용하다.
// 바이너리 파일 내용을 출력하는 스트림을 이용한 예제
var fs = require('fs');
fs.createReadStream('./data/customers.csv').pipe(process.stdout);
Node.js의 콜백
콜백은 Node.js 코드를 비동기적으로 만들 수 있지만 JAVA나 PHP로 작업하는 자바스크립트가 익숙치 않은 나와 같은 프로그래머들은 Callback Hell에서 설명하고 있는 Node.js 코드를 보면 당황할 수도 있다.
fs.readdir(source, function(err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function(filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function(errm values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function(width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(destination + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
이 코드는 Node.js에서 파일 시스템을 다루고 이미지를 조작하는 작업을 수행하는 것으로 보이는데 여기서 gm은 GraphicsMagick 또는 ImageMagick를 사용하는 이미지 조작 라이브러리다.
- fs.readdir(source, function(err, files) {...}): source 경로에서 파일 목록을 읽어오며 콜백 함수를 통해 에러가 발생하면 에러를 로깅하고 그렇지 않으면 파일 목록에 대한 반복 작업을 시작한다.
- files.forEach(function(filename, fileIndex) {...}): 읽어온 파일 목록에 대한 각 파일에 대한 처리를 진행하는 반복 작업을 수행한다.
- gm(source + filename).size(function(errm, values) {...}): gm 모듈을 사용하여 이미지의 크기를 확인하는데 에러가 발생하면 에러를 로깅하고 그렇지 않으면 이미지의 너비와 높이에 대한 정보를 얻는다.
- aspect = (values.width / values.height): 이미지의 가로 너비와 세로 높이를 이용하여 가로세로 비율을 계산한다.
- widths.forEach(function(width, widthIndex) {...}.bind(this)): widths 배열에 정의된 여러 폭에 대해 이미지의 높이를 계산하고, 각 폭에 대해 이미지를 리사이징하여 지정된 폴더에 저장하는 반복 작업을 수행한다.
- this.resize(width, height).write(destination + 'w' + width + '_' + filename, function(err) {...}): 이미지를 리사이징하고 지정된 폴더에 새로운 파일로 저장하며 에러가 발생하면 에러를 로깅한다.
콜백 코드는 promise 객체, 또는 async/await와 같은 패턴으로 대체될 수 있는데 관련 내용은 추후에 다루도록 하겠다.
HTTP Node.js 모듈을 이용한 Hello World 서버
서버 객체 생성 및 요청 핸들러(req와 res 인자를 갖는 함수) 정의와 수신부에 데이터를 전달하고 이 모든 것을 실행시킬 hello.js 예제를 작성해보자.
var http = require('http'); // 핵심 모듈인 http 로드
http.createServer(function (req, res) { // 응답 핸들러 코드를 포함한 콜백 함수를 가진 서버 생성
res.writeHead(200, {'Content-Type': 'text/plain'}); // 적합한 헤더 및 상태 코드
res.end('Hello World!\n'); // Hello World! 출력
}).listen(1337, 'localhost'); // 서버 요청 대기
console.log('Server running at localhost:1337/');
터미널에서 server.js가 위치한 폴더에서 $ node hello.js
커맨드를 실행시킨다. 윈도우의 경우는 Ctrl + C
를, 윈도우의 경우는 ⌃(control) + C
를 입력하여 서버를 종료할 수 있다.
위와 같이 localhost:1337
에 접속하면 Hello World!가 정상적으로 출력되는 것을 볼 수 있다.
Node.js 프로그램 디버깅하기
Node.js에서 디버깅할 수 있는 방법은 많다.
- Core Node.js Debugger: 어디서나 동작 가능한 GUI가 없는 최소한의 기능을 갖춘 도구
- Node Inspector: 구글 크롬 개발자 도구의 디버거 인터페이스
- 웹스톰과 다른 IDE들
가장 좋은 디버거는 console.log()다. 흐름을 방해하거나 깨지 않으며 원하는 정보를 빠르게 제공받을 수 있기 때문이다.
다음은 위에서 작성한 hello.js에 인스턴스가 생성될 때와 요청될 때 두 곳에 debugger를 추가해서 코드를 좀 더 향상시킨 hello-debug.js이다.
var http = require('http');
debugger; // 인스턴스 생성 디버거
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
debugger; // 인스턴스 요청 디버거
res.end('Hello World!\n');
}).listen(1337, 'localhost');
console.log('Server running at localhost:1337/');
$ node hello-debug.js
커맨드로 실행하면 hello.js와 동일하게 실행된다. $ node inspect hello-debug.js
커맨드로 실행해야 실행이 첫 번째 라인에서 멈추고 cont 혹은 c
커맨드를 사용하면 다음과 같이 debugger문에서 다시 멈출 것이다.
주요 node 디버깅 커맨드는 다음과 같다.
- next, n: 다음 문장으로 넘어간다.
- cont, c: 다음 중단점까지 계속 실행한다.
- step, s: 함수 내부로 들어간다.
- out, o: 함수에서 빠져나온다.
- watch(수식): 수식을 확인한다.
커맨드 전체 리스트는 help 커맨드를 통해 확인할 수 있다.
결론
이런 웹 애플리케이션 프레임워크 관련 언어를 여러 개 찍먹하는 것보다 하나의 언어를 선택하여 깊게 파는 것이 좋다는 얘기를 자주 듣는다. 하지만 나는 자바 스프링 프레임워크로 웹 애플리케이션을 개발해 본 경험이 있지만 아직 그렇게 깊게 공부해 보진 않아서 언어를 선택하는 느낌으로 접근한다고 생각하며 공부를 시작했다.
MVC 패턴과 같은 여러 지식이 부족한 현재 상황을 보면 도움을 받을 수 있을 때 받을 수 있도록 Node.js 언어를 공부하는 것이 더 이득이 될 것이라고 판단되었다. 자바스크립트 언어도 아직 잘 모르는 영역이기에 더 열심히 공부해야겠다는 생각이 들었다.
다음 글은 Express.js에 대한 내용일 것이다.
'StudyNote' 카테고리의 다른 글
Node.js와 Express.js 가볍게 입문해보기 - 4 (0) | 2024.01.19 |
---|---|
Express.js와 Prisma ORM + MySQL (0) | 2024.01.15 |
Node.js와 Express.js 가볍게 입문해보기 - 3 (1) | 2024.01.15 |
Node.js와 Express.js 가볍게 입문해보기 - 2 (0) | 2024.01.14 |
도커(Docker) 가볍게 입문해보기 - 1 (1) | 2024.01.13 |