개요

웹 포트폴리오를 만들기 위한 과정을 기록하려고 한다.

기록하는 이유는 그냥 내가 기억하려고... 누구 보라고 쓰는게 아니라서 가독성이 떨어질 수 있다.

 

웹기술 관련 업계 동향

웹만큼 빠르게 변화하는 업계는 없는 것 같다. 개인적으로 생각하는 업계 동향을 적어본다.

 

Front-end

프론트라는 개념이 나온 건 얼마 안됐다. 모든 마크업은 원래 서버의 담당이었다. 브라우저는 그저 그려낼 뿐.

동적인 웹의 필요성을 충족하기 위해 Javascript가 존재하긴 했지만 지금처럼 고도화 된 목적은 아니었다.

그러나 브라우저에서 점점 많은 걸 하게 되고 Ajax, jQuery 등을 거치면서 결국 프레임워크까지 오게 된다.

Angular(구글), React(페이스북)가 시작을 끊었고, 이보다 더 가벼움을 추구하는 Vue, Backbone등도 등장했다.

 

 

Back-end

원래 소규모 사이트는 PHP로, 좀 규모가 좀 된다 하면 ASP/JSP로 개발하는 것이 오랜 시간 지속되어 왔다.

다만 ASP는 IIS를 위해 돈이 좀 들어가므로 JSP 또는 Java 기반(EJB, ...)으로 작성하는 것이 선호되었다.

그러다 로드 존슨이 Java Enterprise의 단점들을 극복하는 책을 내면서 Java Spring이 등장했다.

또 어느 순간 스크립트 언어들이 우후죽순 생기며 다양한 백엔드가 등장했다.

PHP(Laravel 또는 Codeigniter), Python(Django), Ruby(Ruby on Rails) 등...

정점으로 Google V8 Engine이 퍼블릭으로 공개되면서 Javascript 또한 Node.js도 백엔드 대전에 참여했다.

 

포트폴리오 준비 계획

Kakao, Naver, LINE 채용 페이지에서 과연 어떤 경력직을 모집중인지 조사했다.

그리고 Web 기술이 아닌 것들은 배제했다. (핀테크, Video Streaming, Machine Learning 등)

 

대체로 요구하는 기술은 아래와 같다.

[공통]

RESTful API 개념, MSA 개념, MVC 패턴

형상관리 (Git)

Single Page Application (SPA) 개념

Linux, Shell Script, MySQL, NoSQL

 

[JavaScript]

ECMAScript5 (2015), TypeScript

Node.js, MongoDB, Express

jQuery, Backbone.js, React(+Redux), Angular, Vue.js

webpack, Electron

Bootstrap

 

[PHP]

Laravel

 

[Java]

Spring, Playframework(Scala)

 

얼추 프론트는 React, Angular, Vue 세 가지를 골고루 채용하고 있었고, 백엔드는 Spring과 Laravel을 채용하고 있다.

나에게 주어진 한달 남짓의 기간에 이걸 전부 공부하기에는 시간이 모자라다는 판단이 든다. 선택과 집중의 시간.

 

프론트와 백엔드에서 결정하기가 어렵다. 둘 다 취업을 열어놓고 공부를 해야 할 것 같고...

그렇다면 Node.js로 백엔드 공부를, 빠르게 배울 수 있는 Vue로 입문을 하고자 한다. 언어가 같아서 효율적이기도 하고.

 

웹 관련 개념

RESTful API

HTTP Request의 GET/POST/PUT/DELETE를 이용해서 CRUD(Create, Read, Update, Delete) 기능을 구현하는 것이다.

기능적인 측면만 봤을 때는 위와 같고, 사실 그 뒷면에 이념이나 규칙 등은 조금 더 자세하다. 그 내용은 아래 소스들 참고.
 
https://meetup.toast.com/posts/92 (REST API 제대로 알고 사용하기)
 
CRUD를 테스트하는 툴로 Postman을 많이 사용한다. (https://www.getpostman.com)
 

MSA (Microservice Architecture)

서비스를 작은 단위로 쪼개서 운영하는 구조. 반대말은 Monolithic Architecture이다. (앞으로 Mono와 Micro라고 하겠다.)

 

Monolithic 아키텍처는 쉽게 말하면 한 웹서버 안에 모든 기능을 다 때려박은 구조이다. 단순하므로 개발이 빠르고 쉽다. 장점들은 많다.

 

그런데 몇 가지 문제가 있다. (쿠팡 블로그에서 참조함)

1) 일부가 전체에 영향을 미친다. 코드에 메모리 누수가 발생하면 그 서버 전체가 다운되는 수가 있다.

2) 부분적인 Scale-out이 안된다. 일부 서비스에만 오버로드가 발생한다 해도 전체의 스케일을 키워야 한다.

3) 여러 컴포넌트가 강하게 연결되어 있어, 변경이 어렵다. 강결합의 단점은 코드 수정 시 다른 코드에도 영향이 갈 수 있고, 그 정도를 모른다는 점이다.

4) 일부 수정에도 높은 테스트 비용이 든다. 특정 부분만 수정해도 테스트는 전체 서비스에 대해 이뤄지므로 비용이 비싸다.

5) 서비스 규모가 커질 수록 배포 시간이 오래 걸린다.

 

따라서 서비스를 작은 단위로 쪼개고, 물리적인 서버도 분리하고, 각 서비스끼리는 Message Queue를 두어 통신하게 한다.

 

이외에 자세한 내용은 아래 소스들 참고.

 

https://alwayspr.tistory.com/20 (Microservice Architecture 란?)

https://jason-lim.tistory.com/1 (Microservice Architecture에 대한 이야기 - Integration)

https://medium.com/coupang-tech/행복을-찾기-위한-우리의-여정-94678fe9eb61 (쿠팡의 MSA — Part 1)

 

MVC 패턴

개발할 때 Model-View-Controller 3가지 형태로 역할을 분리하여 개발하는 방법론.

 

처음에는 '왜 굳이 번거롭게?' 라는 생각이 들 수 있으나, 나중에는 합리적이라는 생각이 든다.

물론 개발 과정이 기존처럼 파일 하나에 다 때려박던 것보다 번거로운건 사실인데, 이렇게 분리함으로써 나중에 관리하기가 편해진다.

 

[Model] 내부 비즈니스 로직을 맡는 역할. 알고리즘, DB, 데이터 관리 등

[Controller] Model과 View를 연결, 또는 유저와 연결되는 역할. 유저의 명령을 받아 Model에게 일을 넘겨주고 그 결과를 View를 통해 보여줌.

[View] 화면에 무언가 보여주기 위한 역할.

 

각자 하는 역할이 분리된다. 좀 더 자세히 설명하자면

Model은 데이터 혹은 작업 요청서(?)만 받아서 자기 일을 하고 Controller에게 주면 된다. 사용자랑 무슨 일이 벌어지는지는 관심 없다.

View는 Controller가 요청하면 자신의 모양대로 데이터를 그려내면 된다.

Controller는 Model-View를 중간에서 관리하면서, 유저와 연결되어 있기도 하다.

예)

사용자는 Controller에게 요청을 한다.

Controller는 필요에 따라 Model 에게 작업 혹은 데이터를 주어 처리한다.

그리고 그 결과를 View에게 전달하여 사용자에게 화면을 보여준다.

 

더 자세한 내용은 아래 소스들 참고.

 

https://medium.com/@jang.wangsu/디자인패턴-mvc-패턴이란-1d74fac6e256 ([디자인패턴] MVC 패턴이란?)

https://jayzzz.tistory.com/40 (MVC패턴 모델, 뷰, 컨트롤러)

 

Single Page Application (SPA)

이름 그대로 페이지 하나에 모든 기능을 때려박는(?) 접근 방법이다. 모던 웹의 패러다임이라고 할 수 있다. 페이스북이 이런 식이다.

 

장점은 페이지 로드 처음에 일단 모든 리소스를 다운받고, 이후에는 변하는 부분만 서버에서 내용을 받아와 표시한다.

따라서 새로고침이 발생하지 않으므로 Native App과 같은 사용자 경험을 줄 수 있다. 트래픽이 줄어들어 모바일 환경에서 좋다.

 

단점은 최초 1회 로딩에 대하여 많은 양의 리소스를 받아야 하므로 초기로딩속도가 느려지게 된다.

더불어 내용이 상황에 따라 동적으로 변하기 때문에, 검색엔진에서 크롤링이 불가능하다.

 

이외에 장단점이 더 많이 있다. 역시나 아래 소스들 참고.

 

https://poiemaweb.com/js-spa (Single Page Application & Routing)

https://medium.com/@bbirec/spa-single-page-application-개발에서-고려할-사항-eedcb7cb618f (SPA 개발에서 고려할 사항)

https://m.mkexdev.net/374?fbclid=IwAR3H9b90QaKAuuNPgJMfoXnWwfsnU4glF4uBp0QSDhAlnBJ8gNb8D5cdeKQ (SPA 단점에 대한 단상)

 

 


웹 기술 공부

Node.js (Back-End)

Server-Side에서 동작하며 Network Application을 제작할 수 있는 플랫폼이다. 언어는 JavaScript를 사용한다. (Google V8 엔진 기반)

 

혼동하지 말 점은, Node.js는 Web Server가 아니라는 점이다. (Apache가 아니다!)

그냥 JavaScript를 실행(Run)할 수 있게 해 주는 런타임(runtime)이다. 마치 Python처럼 말이다.

 

Node.js가 웹 서버로서 동작하려면 라이브러리(주로 Express)를 이용하여 HTTP 서버 코드를 직접 작성해야 한다.

여기서 직접 작성한다는 말은, Apache가 FTP로 파일을 툭 올렸을 때 폴더 기반으로 파일의 URL을 알아서 처리해 줬던 것과는 달리

Node.js는 URI를 해석하는 것부터, 어떤 파일과 매칭시킬 지 혹은 어떤 코드를 실행시킬 지를 코드로 일일이 라우팅(Routing) 해 주어야 한다.

 

기존 Apache+PHP 환경에 익숙한 사람은 Node.js가 다소 불편할 수도 있다. 지향하는 바가 조금 다른 것 같다.

 

Apache는 FTP로 올린 파일의 경로가 URL에도 1:1로 매칭되는 시스템이다. 즉, 파일 위치가 곧 URL이다.

Node.js는 파일의 위치와 URI가 다른 경우가 허다하다. URI와 파일을 직접 매칭 시켜줘야 한다. 그래서 번거로울 수도 있다.

 

앗, URL과 URI라고 다르게 표기한 것을 눈치챘는가?

더보기

URL의 L은 Locator, 다시 말하면 URL이 파일의 위치를 나타내는 의미를 갖는다.

URI의 I는 Identifier, 위치가 아니라 식별(?) 역할을 하게 된다.

 

URI의 예시:

http://daum.net/login 에서, login 부분은 '기능'을 나타낸다. login 이라는 파일은 없다!

https://www.facebook.com/acuworld 에서, 아이디 부분은 당연히 파일이 아닐 것이다. acuworld는 '정보'를 의미한다.

 

 

URI를 사용하여 파일과는 상관 없이 기능을 자유자재로 매핑시킬 수 있다는 점은 Node.js의 장점이다.

(물론 Apache에서도 rewrite 모듈을 사용하면 가능하긴 하다...)

 

 

입문(Tutorial)

Node.js는 공식적인 튜토리얼이 없고, API 문서만 제공한다.

따라서 외부 튜토리얼로 공부해야 한다. 나는 다음 2개의 튜토리얼을 통해 학습하였다.

 

https://www.w3schools.com/nodejs (영문)

https://velopert.com/node-js-tutorials (한글)

https://backend-intro.vlpt.us (백엔드 기초 다지기, 프로젝트 시작하기)

 

 

설치

학습을 위해 Amazon AWS에 가입하고 프리티어(1년)를 받아 가상서버에서 진행하였다.

Red Hat 리눅스에 설치하였고, 버전은 Node.js 11.10.0 (npm 6.7.0)이다.

 

엔터프라이즈(RedHat, CentOS, Fedora)는 NodeSource라는 곳에서 바이너리를 배포한다고 한다. (공식 사이트의 안내사항이다.)

아래 링크에서 쉽게 설치하는 쉘스크립트를 제공하고 있다. 명령어 한 줄로 Node.js를 설치할 수 있다.

https://github.com/nodesource/distributions/blob/master/README.md

 

 

Hello World 찍어보기

다음 코드를 hello.js에 작성해본다. (vim 이용)

 

console.log("Hello, World!");

 

node를 실행하는 명령어는  node 이다. hello.js를 실행하려면  node hello.js 를 입력하면 된다.

 

 

아주 간단한 웹서버 만들기

웹서버를 만들기 전에, HTTP 프로토콜에 대한 기반 지식이 필요하다. 특히 헤더의 구조에 대해 알고 있어야 한다.

 

아래 코드는 http 모듈을 불러오고, 8080 포트에 http 서버를 오픈하는 예제이다.

그 어떤 URI로 접속하던지 동일한 결과를 출력할 것이다. Header에 Response Code: 200(성공)을 보내고, Body는 Hello World.

 

var http = require("http");

 

http.createServer(function(request,response) {

    response.writeHead(200, {'Content-Type': 'text/plain'});

    response.end("Hello World\n");

}).listen(8080);

 

코드를 해석한다. createServer()는 웹서버 객체(Object)를 만든다. 그리고 객체를 만들 때 익명 함수를 작성하여 전달했다.

더불어 웹서버 객체에 대해 8080 포트로 요청을 받아들이도록 listen() 메소드를 호출했다.

 

이제 해당 웹서버는 8080 포트로 접속이 들어올 때마다, 등록한 익명 함수를 호출할 것이다.

 

웹서버 예제는 더 복잡한 거 말고 여기까지만 하는 게 좋다. 어차피 Express 모듈을 써서 웹서버를 만들 것이기 때문.

 

 

Node Package Manager (NPM)

NPM은 패키지/모듈을 쉽게 설치하고 관리할 수 있도록 해 주는 명령어이다. (RedHat의 yum, Ubuntu의 apt-get과 같은 역할)

 

Node.js로 사이트(프로젝트)를 만들 때 수많은 모듈을 짜집기해서 만들게 된다. 따라서 필연적으로 다음과 같은 2개의 문제점이 발생한다.

 

1) 프로젝트마다 필요한 모듈이 다를 것이다.

2) 프로젝트의 개발 시점에 따라서 사용한 모듈의 버전이 달라질 것이다.

 

따라서 이 프로젝트에 어떤 모듈이 사용됐는지, 모듈의 버전이 무엇인지를 프로젝트 내 package.json 파일에 기록해야 한다.

물론 package.json은 NPM에 의해 자동으로 관리된다. 그런 역할을 NPM이 해 주고 있다.

 

만약 다른 사람의 프로젝트 파일을 다운받았다고 가정해보자. NPM에서 제공하는 간단한 명령어 2~3줄로

해당 프로젝트의 의존성(Dependency) 패키지들을 한 번에 설치하고, 프로젝트를 바로 실행할 수 있다. 

 

 

# 프로젝트 생성

npm init

 

리눅스에서 적당히 폴더를 하나 만들고, 위 명령어를 치면 이제부터 해당 폴더가 프로젝트 폴더가 된다.

프로젝트의 이름, 버전, 제작자 등의 정보를 기입할 수 있다. 귀찮으면 전부 엔터를 쳐버려도 된다.

 

 

# 특정 모듈 설치

npm install [모듈명]

 

# 현재 프로젝트의 모든 Dependencies 모듈 설치

npm install

 

위 명령어는 모듈 파일을 다운받아 프로젝트 폴더에 설치해 주지만, package.json에는 기록하지 않는다.

 

--save 옵션을 주어야 package.json에 "Dependencies" 항목으로 기록된다.

--save-dev 옵션을 주면 "DevDependencies" 항목으로 기록된다. (음... 쉽게 얘기하면 개발용과 배포용을 따로 사용할 수 있다.)

--dev 옵션을 주면 "DevDependencies" 항목의 패키지를 설치한다. 옵션 안주면 일반 Dependencies만 설치된다.

-g 옵션을 주면 글로벌로 설치한다. 기본값은 로컬 설치(현재 프로젝트 폴더에만 설치)이다. 글로벌 설치는 아예 시스템에 파일이 박힌다. (sudo 필요)

 

 

이외에도 NPM의 다양한 기능들이 있다.

예를 들어 npm run-script 명령어는 아주 편하고 많이 사용되는 기능이다. script 관련 내용은 아래 package.json에서 익힐 수 있다.

 

 

package.json

다음 블로그에서 잘 설명해 주고 있다:

https://programmingsummaries.tistory.com/385 (모두 알지만 모두 모르는 package.json)

 

 

Callback Function(콜백 함수) 개념

이 개념을 받아들이기 어려워 하는 사람도 있다. 생소하기 때문이다.

자세한 설명은 다른 블로그가 더 잘 해주고 있고, 여기서는 개요만 설명하겠다.

 

 

기존 Blocking 코딩에서, get_number() 함수가 처리에 5초 걸린다고 가정해 보겠다.

 

var a = get_number(); // 5초 걸리는 놈

console.log(a); // get_number()의 결과를 출력

 

next_function();

 

프로그램의 실행 흐름은 다음과 같다:

get_number() 호출 --> 5초 대기(blocking) --> a 출력 --> next_function(); 실행

 

즉, next_function() 입장에서는 앞 코드 실행이 끝날 때까지 기다려야 자기가 실행될 수 있다.

 

 

다음 예제는 Node.js가 익명 함수를 사용하여 Blocking을 없애고 있는 모습이다.

 

get_number(  function(req,res) { console.log(req); }  );

next_function();

 

get_number() 호출 --> next_function() 호출 --> 5초 후 --> 익명 함수(function)를 호출해서 결과 출력

 

next_function()은 아무런 대기 시간 없이 즉시 실행될 수 있다.

get_number() 함수가 끝났을 때 무슨 코드를 실행할 지를, 익명 함수를 통해 작성하면 된다. (Non-blocking 구현)

익명 함수는 본 함수의 처리가 끝났을 때 호출될 것이다.

 

 

자, 지금까지는 기존 언어(Java 등)에 익숙했기 때문에 '익명 함수' 라고 불렀지만, 사실은 이게 '콜백 함수'이다.

처리가 끝나면 불려지는 함수를 콜백 함수라고 한다.

 

 

모듈 시스템

JavaScript는 모듈 시스템이 없었다. <script src="jquery.js"> 와 같이 페이지 내에 다수의 스크립트를 한 번에 로드해야 했다.

이렇게 하면 단점은 다른 모듈의 코드나 변수에 접근이 가능하다는 점이다.

 

ECMAScript 2015(ES6)부터 모듈 개념이 도입되었다. 모듈은 각각이 개별 프로그램이라는 느낌이 든다. 변수나 패키지도 그 안에서만 사용된다.

문제는 Node.js의 모듈 시스템과 ES6의 모듈 시스템이 호환이 안된다는 점이다.

 

정확하게는 Node.js 모듈 시스템(require)이 자체적으로 먼저 가지고 있었고, ES6 모듈 시스템(import)이 나중에 나온 것이다.

현재까지도 Node.js는 ES6의 모듈(import) 문법을 지원하지 않는다. Node.js에서 import 문법을 쓰려면 Babel 툴로 컴파일을 해야 한다.

 

Babel은 ES6/ES7 문법을 ES5로 transpiling 해 주는 컴파일러이다. 일부 브라우저에서 ES5 까지밖에 지원을 안하므로 이런 툴을 써야 하는 것이다.

 

 


Express 모듈

Node.js로 웹 어플리케이션을 만들기 위한 프레임워크이다. 다양한 웹 관련 기능들을 제공한다.

 

설치

 npm install express --save  으로 간단하게 할 수 있다.

 

 

입문(Tutorial)

대부분의 내용은 아래 소스를 통해 공부하였고, 본 블로그에는 생소한 개념이나 어려웠던 점 위주로만 기록하고자 한다.

 

https://expressjs.com/ko/starter/hello-world.html

https://velopert.com/node-js-tutorials

 

 

라우팅(Routing)

새로운 개념이다.

기존에 Apache+PHP를 사용할 때는 코드를 PHP 파일로 작성하고 FTP로 서버에 올리면 끝이었다. FTP에 올린 경로가 곧 URL이었다.

Apache는 URL을 해석하여 해당 위치의 파일을 PHP 런타임에 전달하고, 결과로 출력된 HTML 코드를 그대로 사용자에게 보내주었다.

 

Node.js에서는 URL이 파일의 위치를 의미하지 않는다. (물론 파일을 의미하게 할 수도 있지만...)

내가 직접 URL을 분석하여, 어떤 행동을 할 지 또는 어떤 파일을 출력할 지를 일일이 결정해 주어야 한다.

 

HTTP 메소드(GET/POST/PUT/DELETE)의 종류에 따라 라우팅이 가능하다.

 

예를 들어 다음 코드는 GET 명령으로 "/" URI가 들어왔을 때, 어떤 함수를 실행할 지를 등록하는 코드이다.

app.get('/', function (req, res) {

    res.send('Hello World!');

});

 

 

Express 애플리케이션 생성기

예제를 풀거나 공부를 할 때는 Single File에서 코드를 작성하였다. 실제로 서비스를 개발하려면 파일을 역할별로 분리하여 관리하는 게 합리적일 것이다.

Express 앱 생성기를 이용하면 기본적인 폴더 구조를 알아서 만들어준다. 일종의 템플릿이라고 보면 된다.

 

폴더는 public(정적 파일), routes, views, bin 으로 나눠진다.

 

간단히  express --view=pug myapp  명령을 입력하면 myapp 디렉토리가 생성되며 Express App이 생성된다. (템플릿 엔진으로 pug를 설정)

해당 디렉토리로 이동 후  npm install  로 기본적인 패키지를 설치한다. 이후  DEBUG=myapp:* npm start  명령어로 프로젝트를 실행해 볼 수 있다.

 

자세한 내용은 다음 링크 참고: https://expressjs.com/ko/starter/generator.html

 

 

템플릿 엔진(Template Engine)

각종 설명들을 요약하면 HTML 부분과 DATA 부분을 분리하기 위해 사용한다고 한다.

결과를 어떻게 표시할지를 Template으로 만들어두고, 여기에 Data만 집어넣으면 HTML 결과가 나오는 것이다.

 

사람의 입장에서 보면, 웹 퍼블리셔와 백엔드 개발자의 역할을 분리할 수 있다.

웹 퍼블리셔는 HTML/CSS로만 디자인을 하고, 데이터가 들어갈 곳에는 <% 변수명 %> 등의 문법으로 자리만 잡아주면 된다.

백엔드 개발자는 Database에 쿼리를 쏴 데이터를 추출하고, 변수명만 잘 맞춰서 Template에 입력하기만 하면 된다.

 

브라우저 입장에서 보면, 쓸데없는 데이터 재전송을 막을 수 있다.

사이트에서 일부 내용만 바뀌는 경우(예: 페이스북) 기존 방식대로라면 페이지 전체를 다시 렌더링 해야 한다.

템플릿을 적용하면 데이터가 변한 그 부분만 내용을 수정할 수 있어 렌더링 시간과 네트워크 대역폭을 절약할 수 있다.

 

자세한 내용은 다음 소스들 참고:

https://gmlwjd9405.github.io/2018/12/21/template-engine.html (템플릿 엔진(Template Engine)이란)

https://show-me-the-money.tistory.com/56 (템플릿 엔진이란 무엇인가?)

https://nesoy.github.io/articles/2017-03/web-template (웹 템플릿 엔진(Template Engine) 이란?)

 

 


MongoDB

MySQL은 Relational Database이다. 데이터의 규격 및 관계가 명확하게 정의된 채 사용되는 데이터베이스이다.

이러한 관계형 DB는 SQL(Structured Query Language) 이라는 언어를 사용하며, ACID(Atomic, Consistency, Integrity, Duarabity) 특성을 지원한다.

ㅋ... 쉽게 말하면 데이터의 신뢰가 보장되지만 구조의 유연성이 떨어진다고 요약할 수 있다. 정확한 데이터 처리를 요하는 은행, ERP 등에는 필수이다.

 

시대가 흐르면서 Non-Relational Database가 등장한다. 비정형 데이터를 쉽게 담아서 쓰는 NoSQL 이라고 부른다.

차이점은 관계형 DB보다 융통성 있고 유연한 데이터 모델을 사용하며, 데이터의 저장 및 탐색에 특화된 알고리즘을 사용한다.

ㅋ... 이것도 요약하면 관계성을 포기한 대신 단순 저장&탐색에는 아주 빠른 성능을 보인다는 것이다. 대용량 데이터(페북, 트위터 등)를 다룰 때 좋다. 

 

MongoDB는 NoSQL에 해당한다.

NoSQL에는 여러 종류의 데이터 모델(Model)이 존재하는데, Key Value DB, Big Table DB, Document DB, Graph DB 등이 있다.

이 중 MongoDB는 DocumentDB에 해당한다. DocumentDB는 데이터가 JSON, XML 등의 Document로 모델링 되어있는 형태이다.

 

다양한 NoSQL별 특징 및 성능 비교 등등 정보는 다음 링크 참고:

https://www.samsungsds.com/global/ko/support/insights/1195843_2284.html (NoSQL이란 무엇인가? 대량데이터 동시처리위한 DBMS 종류와 특징)

https://www.mongodb.com/compare/mongodb-mysql (MongoDB and MySQL Compared)

 

 

입문(Tutorial)

공식 튜토리얼(영문): https://mongoosejs.com/docs/index.html

한국어 강좌 링크: https://velopert.com/mongodb-tutorial-list

 

우선 용어가 조금 다른데, MySQL의 테이블, 로우, 컬럼을 MongoDB에서는 컬렉션, 도큐먼트, 필드 라고 부른다.

더 자세한 개념은 위 링크들 참고.

 

 

간단한 커맨드들

설치 후  mongo  명령어로 Command Line에 접속할 수 있다.

 

Database를 생성하려면  use 디비명  을 이용하면 된다. 왜 커맨드가 use인가?

이미 DB가 존재한다면 있던 걸 사용하고, 없으면 새로 생성한다. 단, Document를 하나 이상 생성해야 DB도 생성된다.

 

Database 목록은  show dbs  를 통해 확인하며, 사용중인 디스크 용량까지 볼 수 있다.

 

 

관리 도구: Compass

공식 사이트에서 MongoDB를 관리할 수 있는 GUI 프로그램을 제공하고 있다. (MySQL의 Workbench 같은 느낌)

 

다운로드 링크: https://www.mongodb.com/products/compass

 

 

Mongoose

Node.js의 모듈이다. 역시  npm install mongoose --save  로 설치할 수 있다.

Mongoose는 MongoDB에서 부족한 부분들을 다양한 자체 기능으로 메꾸고 있다.

 

스키마(Schema) 기능을 제공한다. NoSQL의 장점이자 단점은 Document에 넣는대로 들어간다는 것이다.

즉, 필드가 정해져 있지 않으므로 실수로 필드명을 오타 내거나, 혹은 다른 자료형으로 데이터를 삽입하는 실수가 일어날 수 있다.

그래서 데이터를 넣기 전에 한 번 필드명과 자료형을 검사해 주도록 하는 기능이 제공된다.

(MySQL에서 Table을 만들 때 column을 미리 정하듯이, Schema를 통해 Document의 field를 미리 정하게 된다.)

 

Populate라는 기능도 있다. 쉽게 말하면 MySQL의 JOIN을 구현한 것이다.

만약 어떤 Document가 다른 Document의 ID를 갖고 있을 때, MySQL에서는 JOIN을 통해 두 Document의 내용을 한 번에 가져올 수 있다.

MongoDB에서는 두 번의 Read를 거쳐야 한다. 첫 번째 Document를 먼저 가져오고, 두 번째 Document를 ID로 조회해서 또 가져와야 한다.

이를 Mongoose가 알아서 해 주는 것이다. 다만 MySQL의 JOIN과는 달리 그냥 대신 해 주는 것에 불과하므로, 남발하면 성능 하락이 있다.

 

이외에도 PromiseQuery Builder 등의 좋은 기능들도 있다.

 

자세한 내용은 다음 링크 참고:

https://www.zerocho.com/category/MongoDB/post/5963b908cebb5e001834680e (Mongoose(몽구스) 시작하기)

https://velopert.com/594

 (Express와 Mongoose를 통해 MongoDB와 연동하여 RESTful API 만들기)

 

 

간단한 예제 1 (DB에 데이터 넣어보기)

위에서 언급한 공식 사이트(영문)에서 다양한 개념을 잘 설명해 주고 있으니 건너 뛰었다면 한 번 정도는 보기를 권한다.

특히 Mongoose 기능 전반에 대해 한 페이지로 요약 설명한 Getting Started 의 내용이 좋다. (https://mongoosejs.com/docs/index.html)

 

먼저 다양한 세부 개념에 대한 설명은 다음 링크 참조:

https://www.zerocho.com/category/MongoDB/post/59a1870210b942001853e250 (Mongoose 스키마(Schema))

https://dalkomit.tistory.com/112 (Mongoose 모델의 강점)

 

Schema는 Document의 필드들을 정의한다. MySQL에서 Table의 Column을 정하는 것이라고 보면 된다. 그런데 Table을 만든 것은 아니다!

그저 Rule을 만든 것이다. 아직까지는 Collection(Table)과는 아무런 관련이 없이, 규칙에 불과하다.

 

방금 만든 Schema를 Model이라는 것으로 컴파일 해야 비로소 Collection의 역할을 하는 것이다. (처음엔 이해가 안될 수도 있음)

 

예제는 심플함을 위해 싱글 파일로 작성하고 실행하였다. 뭔 말이냐면 Express Generator를 쓰지 않았다는 소리다.

npm start니 app.js니... 겨우 예제 푸는데 귀찮게 그러지 않고  node example.js  이런 식으로 싱글 파일로 실행했다.

(아 물론 npm install로 mongoose나 express 등 모듈들은 설치해 줘야 한다.)

 

다음 코드의 예제를 보도록 하자. (공식 사이트의 Getting Started를 다듬었다.)

 

/* Mongoose 객체를 생성함 */ var mongoose = require('mongoose'); /* 서버에 접속 시도 */ // connect() 두 번째 인자로 옵션 준 거는 그냥 주라고 해서 준 거임. 생략해도 무방 mongoose.connect('mongodb://localhost/portfolio', { useNewUrlParser: true }); /* 객체에서 커넥션 정보 가져오기 */ // 접속 실패 시 에러 메시지 띄우기 var db = mongoose.connection; db.on('error', console.error.bind(console, 'connection error:')); db.once('open', function () { // Connected! }); /* 스키마(Schema) 생성 */ var personSchema = new mongoose.Schema({ name: String, age: Number, }); /* 모델(Model) 생성 */ // Collection을 만들었다고 보면 됨 var Person = mongoose.model('Person', personSchema); /* 모델을 이용하여 Document 생성 */ var acu = new Person({ name: 'Acu', age: 26 }); // Document를 객체로써 취급한다. (ODM 개념) console.log(acu.name); // 'Acu' console.log(acu.age); // 26 /* 모델을 실제 DB에 반영 */ acu.save();

 

느낌은 마치 C++에서 클래스를 만들고 객체로 이것저것 데이터 처리를 하는 느낌이다.

 

실제로 MySQL의 데이터를 객체 쓰듯이 쓸 수 있도록 한 ORM(Object Relational Mapper)이 존재한다.

얘도 마찬가지로 똑같은데, 이름은 살짝 다르게 ORM(Object Document Mapper)이다. 즉 객체처럼 쓰는 게 맞다는 것ㅎㅎ

 

코드를 정리하자면, Schema를 만들어서 규칙을 생성하고, 이 규칙을 토대로 Model을 만들어 Collection을 담당하게 한다.

그리고 Model을 기반으로 Document를 만들어 데이터를 넣는다. 마지막으로 실제 DB에 save()로 적용하면 된다.

 

리눅스 MongoDB에서 다음 커맨드로 위 코드의 결과를 알 수 있다.

 

> show dbs

admin      0.000GB

config     0.000GB

local      0.000GB

portfolio  0.000GB

 

> use portfolio

switched to db portfolio

 

> show collections

people

 

> db.people.find({})

{ "_id" : ObjectId("5c7fd76db6c337600a1b7c14"), "name" : "Acu", "age" : 26, "__v" : 0 }

 

Model로 컴파일하면서 Collection의 이름을 지정하게 된다. 예를 들어 var Person = mongoose.model('모델 이름', 스키마);

여기서 모델 이름이 자동으로 Collection의 이름이 되는데, 특이한 점은 자동으로 영어 복수형으로 변환된다.

 

뭔 소리냐? 예를 들어 mongoose.model('Car', carSchema); 를 실행하면 실제 MongoDB에는 Cars 컬렉션이 생성된다.

Person을 입력하면 People이 된다(...).

 

이렇게 Document와 Collection의 이름이 연동되는게 싫으면 별도로 옵션을 주면 된다는데 그것까진 굳이 안찾아봤음.

 

 

간단한 예제 2 (RESTful API 만들기)

이 예제도 역시나 싱글 파일로 작성했다. 하지만 나중에는 Router 파일을 분리하는 등 코드를 체계적으로 관리해야 할 것이다.

 

참고 사이트: https://velopert.com/594 (Express와 Mongoose를 통해 MongoDB와 연동하여 RESTful API 만들기)

 

[사전지식: URL을 통한 정보 전달]

일반적으로 http://google.com/?first=512&second=12512 이런 식으로 URL에 데이터를 전달한다. 이런 걸 Query String 이라고 한다.

정보는 key=value 형식으로 되어 있고, 각 데이터는 &로 구분된다.

 

다른 방식으로도 전달이 가능한데, http://blog.com/view/12345/mobile 이런 식이다. 이런 걸 Semantic URL 이라고 한다.

정보는 value의 나열로 되어 있고, 각 데이터는 /로 구분된다. 그럼 key는??? 프로그래머가 결정한다.

 

라우팅 시 app.get('/view/:id/:mode') 형태로 받으면 id=12345, mode='mobile' 을 의미하게 된다.

 

내용 출처: https://wayhome25.github.io/nodejs/2017/02/18/nodejs-11-express-query-string/ (Express-URL을 이용한 정보의 전달)

 

[사전지식: JSON Parsing]

REST API는 Header로 GET/POST/PUT/DELETE 중 1개가 오고, Body에는 JSON 데이터가 온다. (안올수도 있지만;;)

이 말은, Body를 파싱할 필요가 있다는 뜻이다. JSON Parser를 이용해서 데이터를 객체(Object)로 만드는 과정이 필요하다.

 

예를 들어 { name: "Acu", age: 50 } 이라는 JSON 데이터가 Body로 도착한 경우, JSON Parser를 통해 파싱을 거치면

body.acu 라던지 body.age 처럼 파싱된 데이터를 body라는 객체에 담아 편리하게 쓸 수 있다.

 

[과거와 달라진 점]

원래는 JSON Body를 파싱하기 위해 body-parser 모듈을 사용하는게 정석이었으나,

Express v4.16.0 부터 Body Parser를 내장했기 때문에 body-parser 모듈이 더 이상 필요치 않다고 한다.

 

var bodyParser = require('body-parser')

app.use(bodyParser().json())

app.post('/', function(req, res) => {

    console.log(req.body)

})

 

이렇게 썼던 기존 코드를

 

app.use(express.json())

app.post('/', function(req, res) => {

  console.log(req.body)

})

 

이렇게 내장 Parser를 이용하여 파싱된 req.body를 얻을 수 있다. (Parser를 쓰지 않으면 req.body는 Undefined 임)

 

코드가 좀 길어서 부분별로 나눴다. 전부 한 파일로 합치면 RESTful API 서버가 된다.

 

더보기

/* 패키지 로드 */ var express = require('express'); var mongoose = require('mongoose'); /* Express 객체 생성 */ var app = express(); app.use(express.json()); // JSON Body Parsing

 

더보기

/* Mongoose 접속 */ var db = mongoose.connection; db.on('error', console.error); // 에러 났을 때 뭐할지 등록 db.once('open', function () { // 접속 시 뭐할지 등록 console.log('Connected to mongod server.'); }); mongoose.connect('mongodb://localhost/portfolio', { // 서버 접속 useNewUrlParser: true }); /* Mongoose Scheme & Model 생성 */ var personSchema = new mongoose.Schema({ name: String, age: Number, }); var Person = mongoose.model('Person', personSchema);

 

더보기

/* 라우팅(Routing) */ app.post('/', function (req, res) { // Body에 {"name":"Hong", "Age":30} 과 같은 JSON이 온다. // 새로운 document 생성 var person = new Person(); person.name = req.body.name; // JSON Parser가 req.body에 담아주었다. person.age = req.body.age; // 실제로 db 서버에 반영 person.save(function (err) { if (err) { console.error(err); res.json({ result: 0 }); // 성공 혹은 실패를 0,1로 반환 return; } res.json({ result: 1 }); // 성공 혹은 실패를 0,1로 반환 }); });

 

더보기

app.get('/', function (req, res) { // 별도의 Data를 받지 않는다. DB의 모든 데이터를 출력 // find()에 아무런 쿼리가 없으면 모든 document를 가져온다 Person.find(function (err, data) { if (err) return res.status(500).send({ error: 'fail.' }); res.json(data); }) }); app.get('/:name', function (req, res) { // Semantic URL 방식으로 name을 받았다. DB에서 name을 찾아 정보를 출력 // 1번째 인자는 쿼리, 2번째 인자는 Projection(어떤 필드를 보이고 숨길 지) Person.find({ // (1)쿼리 name: req.params.name }, { // (2)프로젝션 _id: 0, name: 1, age: 1 }, function (err, data) { // (3)콜백 함수 if (err) return res.status(500).send({ error: 'fail.' }); res.json(data); }); });

 

더보기

app.put('/:name/:new_age', function (req, res) { // 먼저 name에 해당하는 document를 찾는다 Person.findOne({ name: req.params.name }, function (err, data) { if (err) return res.status(500).send({ error: 'fail.' }); if (!data) return res.status(404).json({ error: 'data not found' }); // document를 새 데이터로 Update data.age = req.params.new_age; // document 저장 data.save(function (err) { if (err) res.status(500).json({ error: 'failed to update' }); res.json({ message: 'data updated' }); }); }); // 만약 Person.find()로 찾으면 data는 Array로 반환되기 때문에 data.save()가 불가능하다. (1개 고르면 되긴 함) // Person.findOne()을 쓰면 data는 1개의 Document기 때문에 data.save()가 가능하다. // find() 후 save() 하는 방법 이외에도 // Person.update()나 Person.findOneAndUpdate() 등 업데이트 방법은 다양하게 있다. });

 

더보기

app.delete('/:name', function (req, res) { Person.remove({ name: req.params.name }, function (err, output) { if (err) return res.status(500).json({ error: "fail" }); res.status(204).end(); }) });

 

더보기
/* Port 3000에 웹서버 시작 */ app.listen(3000, function () { console.log('Listening on port 3000!'); 

});

 

코드를 보면 알겠지만, 데이터를 전달하는 방법으로 두 가지가 있는데

  1. Body에 JSON으로 전달하는 방법

  2. URL에 Param으로 전달하는 방법 (Semantic URL)

이 있다. 정답은 딱히 없겠지만, 데이터가 많으면 1번으로, 그렇지 않고 대표성이 있다면 2번을 취하는 게 좋은 것 같다.

 

마침 비슷한 고민을 한 사람이 있다: https://blog.outsider.ne.kr/1116 (Stackoverflow, Twitter, Github, Medium의 URL 패턴 간단 정리)

 


실제로 뭔가를 만들기 위해 필요한 개념들

Token 기반 REST API

기존에는 서버 구현 시 사용자의 로그인 정보를 서버가 가지고 있었다. 이를 세션(Session)이라고 한다.

세션 방식은 서버가 계속 사용자의 정보를 들고 있어야 하므로, 서버를 확장하기 어려운 등 유연성이 제한되는 문제가 있다.

 

비슷하게 브라우저에서도 쿠키(Cookie)를 가지고 자신의 상태를 보관하거나 서버에게서 받은 정보를 담아두었다.

쿠키의 단점은 단일 도메인 혹은 서브 도메인까지만 작동하므로, 도메인이 여러 개인 서비스를 운영 시 쿠키 관리가 어렵다는 점이다.

 

정리하자면 세션(Session)이나 쿠키(Cookie) 모두 '현재 상태', 즉 State를 보관하는 용도로 대부분 사용되기 때문에 단점과 한계점이 많다.

그런데 Token 기반으로 API를 짜면 Stateless로 동작한다. 사용자가 로그인 되어 있는지 등의 상태 관련 정보를 서버가 몰라도 된다는 것이다.

 

더 자세한 개념들은 다음 링크 참고:

https://velopert.com/2350 ([JWT] 토큰(Token) 기반 인증에 대한 소개)

 

 

음... 간단하게 토큰(Token)을 찜질방 비유로 생각해 보았다.

 

[로그인]: 찜질방에 입장할 때 내 이름을 적고, 옷장키(Token)를 받는다. 옷장키는 찜질방 내 결제에 사용할 것이다.

  [Token 생성]: 옷장키(Token) 안에는 (1)내 이름(2)찜질방 주인의 서명이 들어있다. 서명으로 인해 찜질방 주인만 키 생성 가능.

 

[API 이용]: 찜질방에서 계란을 사려고 한다. 카운터(Server)(1)계란을 사겠다는 주문(API)(2)옷장키(Token)를 전달한다.

  [Token 검증]: 찜질방 주인은 옷장키(Token)에 적힌 (1)사람 이름(2)서명을 본다. 서명이 올바르면 키에 적힌 이름으로 구입 내역을 기록한다.

 

[Token 위조]: 누군가 내 이름으로 라면을 사먹으려 시도한다. 빈 옷장키(Token)에 내 이름은 적었으나, 찜질방 주인의 서명을 할 수 없어 실패한다.

[Token 탈취]: 내가 실수로 옷장키(Token)를 두고 자리를 비웠다. 누군가 그 키를 주워 식혜를 사먹었다. 옷장키(Token)는 정상이므로 막을 수 없다.

 

웹으로 다시 말하면, 유저는 로그인 시 서버로부터 Token을 발급받는다. 그리고 이후부터는 API 사용 시마다 Token을 같이 전달하면 된다.

서버는 유저의 로그인 사실을 기억할 필요 없이 API 호출 시마다 Token의 유효성(Valid)만 확인하면 된다. 유저 또한 쿠키 등을 보관할 필요가 없다.

 

문제는, Token 구현에 기본적으로 암호화가 없기 때문에 Token을 탈취 당하면 공격자가 사칭을 할 수 있다.

따라서 Token을 별도로 암호화 하는 기술을 사용하거나, Token의 유효 시간(Expire Time)을 짧게 하여 탈취당한 Token이 빨리 만료되도록 한다.

 

 

JSON Web Token (JWT)

그럼 실제로 Token을 어떻게 구현하고 이를 REST API에 적용할 수 있는가? 웹표준으로 JWT라는 것이 존재한다.

 

다음 사이트에서 Token의 개념을 익힐 수 있다:

https://jwt.io (JSON Web Tokens)

https://velopert.com/2389 ([JWT] JSON Web Token 소개 및 구조)

https://hycszero.tistory.com/108 (JWT(JSON Web Token)로 로그인 / 로그아웃 구현하면서...)

https://victorydntmd.tistory.com/115 ([HTTP/인증] JWT ( JSON Web Token ))

 

JWT는 REST API를 호출할 때 Token을 Header에 낑겨 보내는 방식을 취한다. (별도의 JSON으로 Token을 주는 것이 아니다.)

 

HTTP Header에 실으려면 JSON 형태보다는 단일 String 형태가 적합하다.

따라서 Token의 내용은 비록 JSON으로 작성되지만 최종 단계에서 이를 Base64로 인코딩하여 1개의 String으로 만든다.

 

JWT는 3부분으로 이뤄진다:

Header: 이 Token의 정보를 담고 있다. Token의 해싱 알고리즘이 무엇인지 기록한다.

Payload: 실제 Token의 데이터이다. 사용자 정보, 토큰 생성 시간, 만료 시간 등을 기록한다.

Signature: 서버의 비밀키를 이용해 Header,Payload에 대해 해싱함으로 서명을 만든다. 따라서 비밀키가 없는 외부 공격자는 서명 생성이 불가능하다.

 

----------

뭐 해싱하고 검증하고 이런 작업들이 복잡한데, 우리가 직접 할 필요는 없다. jsonwebtoken 모듈을 사용해서 실습해 볼 것이다.
아래 코드는 Token을 생성하는 코드이다. URL에 원하는 아이디를 입력하면 토큰을 생성한다. (예: http://localhost/create/my_id)

 

var express = require('express'); var jwt = require('jsonwebtoken'); var app = express(); const secretKey = 'aw35gvaw5aw35h112'; app.get('/create/:userid', function (req, res) { var payload = { id: req.params.userid }; var token = jwt.sign(payload, secretKey); // Token 생성 res.send(token); // Token 출력 });  

app.listen(3000, function () {});

 

핵심은 jwt.sign() 함수이다. 원래 비밀키는 길이가 길 수록 좋고 512비트 이상을 권장하지면 여기서는 편하게 아무거나 쳤다.

 

Token의 Payload는 id라는 항목만 집어넣었다. 보이진 않지만 iat(Issued At)라는 이름의 '토큰 생성 시간' 항목이 자동으로 들어가게 된다.

따라서 위의 웹 페이지를 들어가 계속 새로고침하면 Token 내용이 시간에 따라 변하는 것을 알 수 있다.

 

https://jwt.io/ 사이트에 들어가서 왼쪽 Encoded 칸에 우리가 만든 Token을 넣고, 오른쪽에는 Secret Key를 입력하면 즉석 Verification을 해볼 수 있다.

 

----------

이번에는 Token 검증을 해보려 한다. 위의 코드에서 라우팅 하나만 추가하도록 하자. (예: http://localhost/verify/eyJhbGciOiJIUzI1NiIsInR5........)

 

app.get('/verify/:token', function (req, res) { var token = jwt.verify(req.params.token, secretKey); // Token 검증 if (token) res.send('valid!'); else res.send('invalid!'); 

});

 

핵심은 jwt.verify() 함수이다. 검증 결과를 boolean으로 출력한다.

 

----------

만약 여기서 Token에 Expire Time을 10초로 하고 싶다면 Token 생성 부분에 다음과 같이 코드를 바꾸면 된다.

jwt.sign()의 3번째 인자로 Option을 새로 준 것이다.

 

var options = { expiresIn: '10s' }; 

var token = jwt.sign(payload, secretKey, options); // Token 생성

 

위 코드로 생성된 Token은 10초 뒤 Verify를 해 보면 Expired 라고 뜰 것이다.

 

 

더 자세한 실습 등은 다음 링크 참고:

https://victorydntmd.tistory.com/116 ([Node.js] JWT 기반으로 사용자 인증 구현하기 ( jsonwebtoken 모듈 ))

 

 

로그인/회원가입 기능 구현

일단 Front-end는 고려하지 않고, Back-end에서 REST API로 만들 것이다.

 

Node에서도 세션과 쿠키를 이용한 전통적 로그인을 구현할 수 있다. cookie-parser, express-session 모듈을 사용하면 된다.

하지만 여기선 Web Token 기반 로그인을 구현할 것이다. 로그인 시 Token을 발행해 주고, 이후 API 사용 시마다 해당 Token을 받는 형태이다.

 

Token은 탈취당할 수 있기 때문에 Expire Time을 짧게 하고, 대신 Refresh가 가능하도록 할 것이다.

(참고로 `자동 로그인` 기능은 Token의 Expire Time을 없애는 것으로 구현할 수 있다.)

 

만든 API를 이쁘게 Document로 적고 싶으면, 다음 레퍼런스들 참고:

https://bocoup.com/blog/documenting-your-api (REST API DOCUMENTATION BEST PRACTICES)

http://apidocjs.com (Inline Documentation for RESTful web APIs)

https://trello.com/c/lBnwdKRj/22-rest-api-문서화로-어떻게-남길까 (REST API 문서화로 어떻게 남길까)

https://sanghaklee.tistory.com/50 ([Swagger] RESTful API 문서 만들기)

 

다음과 같은 API를 만들 것이다. 간단한 몇 개만 구현한다.

 

Title: 회원 가입

URL: http://localhost:3000/users

Method: POST

Data params:

{

    "u" : {

        "username": [string],

        "password": [string],

        "name": [string],

        "email": [string]

    }

}

Success Response: Code: 200, Content: { result: 1 }

Error Response: Code 422, Content: { msg: [string] }

 

 

Title: 로그인 (Token 받기)

URL: http://localhost:3000/users

Method: GET

 

 

이제 슬슬 번듯한 사이트를 만들 것이므로 Express Generator로 프로젝트를 만들고 구조를 따라가려 한다.

마침 기본으로 users라는 이름의 라우터가 들어 있으므로, routes/users.js에 우리의 API를 구현하면 된다. app.js에 Mongoose 모듈 로드만 해 주자.

 

 

 

 

 

 

 

 

 

 

하 귀찮네

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(공부하면서 계속 내용 추가 중)

(Last Update: 2019/03/11 월요일) 

 

 

 

아아~~ IT기업 상반기에 뽑는데가 없어서 때려침ㅎㅎ 삼성가야지~

심심하면 글 계속 씀

+ Recent posts