2023년 4월 24일 월요일

REACT - NEXTJS 교육 정리

REACT - NEXTJS 교육 정리

1. 주요 기능

- File-based Routing

- Server-side Rendering: SEO 지원, Client/Server Side를 적절히 조정

- Fullstack Capabilities: Backend코드와 연계, 데이터 저장/조회, 인증 등

사용방법

1913


2. 실행 방법

- npx create-next-app


3. 개발 방법

- pages folder에 페이지 별로 js파일을 생성

- 개별 js에는 react import가 불필요

- 하위 폴더를 추가하고 index.js로 지정하면 하위 폴더명으로 라우팅

- [newsId].js는 동적인 페이지로 인식


4. 주요 component

- useRouter : Parameter추출할 수 있음

  router.push('/'+param.id)

- Link : SPA 용 a tag 대용

5. 특별한 기능

- _app.js : Root component로 작동

2019년 3월 10일 일요일

Node.js PM2로 실행하고 프로세스 관리

Node.js PM2로 실행하고 프로세스 관리


프로세스 관리하기 - PM2


PM2 명령어 외에 설정파일로 관리하기

https://blog.rhostem.com/posts/2018-05-27-pm2-deploy

2019년 3월 8일 금요일

Node.js 로그파일 기록하기 - Winston

Node.js 로그파일 기록하기 - Winston

어플리케이션을 실행할 때 로그를 기록하는 것은 실사용에서 꼭 필요한 작업이다.  Node에서 일반적으로 사용하는 로그 모듈인 Winston을 이용하여 Console과 File에 로그를 만드는 방법에 대해 인터넷을 찾아보고 내가 원하는 방식으로 로그파일을 만들어 본다.
내가 원하는 방식:

  1. 로그파일은 \logs 디렉토리 밑에 날짜별로 생성
  2. 로그는 express의 미들웨어를 통해 client call을 로깅
  3. 호출파일에 대한 기록 // 향후 어떤 파일을 호출했는지 찾아보기 위해 적용

단순해 보이지만 이를 모두 구현한 예제를 찾기 힘들었고 여러가지 예제를 조합하여 다음과 같이 예제 프로그램을 구현해 보았다.

디렉토리는 다음과 같이 구성된다.
-- test.js (테스트 파일)
 + configs\winston.js (Winston 설정파일)
 + logs\yyyy-mm-dd-cmpapi.log (로그 파일)

configs\winston.js 파일은 다음과 같다.
  • // Log file lotation - 날짜에 따라 새로운 로그를 생성
  • const { createLogger, format, transports, handleExceptions } = require('winston');

  • const path = require('path');

  • // 시스템의 환경 변수를 확인해서 NODE_ENV값을 확인
  • // 사용은 PM2 의 ecosystem.yaml의 env항목 혹은 docker의 environment 항목에 설정
  • // 이런 설정이 없으면 'development' 로 지정
  • const env = process.env.NODE_ENV || 'development';

  • // logs 디렉토리를 생성하기 위해서 사용
  • const fs = require('fs');

  • // 현재 위치에 log 디렉토리를 지정 : const logDir = __dirname + '/logs';
  • const logDir = 'logs';

  • // Create the log directory if it does not exist
  • if (!fs.existsSync(logDir)) {
  •   fs.mkdirSync(logDir);
  • };

  • // File 모드 옵션 설정 - 날짜별로 로그파일의 이름을 변경
  • require('winston-daily-rotate-file');
  • const dailyRotateFileTransport = new transports.DailyRotateFile({
  • //  level: env === 'development' ? 'verbose' : 'info',
  •   handleExceptions: true,  // Exception 발생 시 로그에 기록
  •   filename: `${logDir}/%DATE%-cmpapi.log`, // 로그파일명
  •   datePattern: 'YYYY-MM-DD', // 로그에 사용되는 날짜 포멧
  •   json: false,
  •   colorize: false,
  •   zippedArchive: true, // 오래된 로그파일 압축
  •   maxSize: 5242880, // 5MB
  •   maxFiles: 5, // 5MB - 5개 파일까지 생성 후 이후부터는 오래된 파일 삭제
  •   format: format.combine(
  •     format.printf(
  •       info =>
  •         `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
  •     )
  •   ) // 로그의 포멧 정의
  • });

  • // Console 모드 옵션 설정 - 로그의 패턴별로 색상을 다르게 지정함
  • const consoleTransport = new transports.Console({
  •   level: env === 'development' ? 'verbose' : 'info',
  •   handleExceptions: true,  // Exception 발생 시 로그에 기록
  •   json: false,
  •   colorize: true,
  •   format: format.combine(
  •     format.printf(
  •       info =>
  •         `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`
  •     )
  •   ) // 로그의 포멧 정의
  • });

  • // logger를 호출할 때 callback으로 filename을 추가하기 위해 구조를 변경
  • const logger = caller => {
  •   return createLogger({
  •     level: env === 'development' ? 'verbose' : 'info',
  •     format: format.combine(
  •       format.label({ label: path.basename(caller) }), //로그에 호출된 파일이름 기록
  •       // format.colorize(),
  •       format.timestamp({
  •         format: 'YYYY-MM-DD HH:mm:ss'  // 로그의 Timestamp 포멧을 지정
  •       }),
  •     ),
  •     transports: [
  •       consoleTransport,  // Console에 표시하는 형태
  •       dailyRotateFileTransport  // File에 표시하는 형태
  •     ],
  •     exitOnError: true // handleExceptions 실행
  •   });
  • };

  • //handleExceptions(new transports.Console({ colorize: true, json: true }));

  • module.exports = logger; 

test.js파일은 다음과 같다.

  • // 테스트를 위하여 express 모듈을 사용
  • const express = require('express')
  • const app = express()
  • const port = 3000
  • let queryString = '';

  • // Winston Setup - Winston은 로그를 기록할 때 일반적으로 사용하는 모듈
  • // configs 디렉토리의  wiston.js에 winston설정을 지정하여 import하여 사용
  • let logger = require('./configs/winston')(__filename);

  • //express에서 logger.steam 호출 시 표시하는 형태
  • logger.stream = {
  •   write: (message, encoding) => {
  •     logger.info(message);
  •   }
  • };

  • // Morgan Setup - Morgan은 Rest API의 로그(req,res)를 보기 좋게 구성
  • let morgan = require('morgan');

  • // Morgan Setup - dev는 날짜 표시를 제외한 Standard Apache common log output
  • app.use(morgan('dev', { "stream": logger.stream }));

  • // Level 별로 다른 로그 지정
  • logger.debug('Debugging info');
  • logger.verbose('Verbose info');
  • logger.info('Hello world');
  • logger.warn('Warning message');
  • logger.error('Error info');

  • // 아래는 테스트를 위한 express route call 수행
  • app.get('/', (req, res) => res.send('Hello World!'))

  • app.get('/pathCall', (req, res) => {
  •   console.log(req.query.queryStr);
  •   switch (req.query.queryStr) {
  •     case 'Location':
  •       queryString = ' - Location Call';
  •       break;
  •     case 'Country' :
  •       queryString = ' - Country Call';
  •       break;
  •     case 'SubDiv' :
  •       queryString = ' - Subdivision Call';
  •       break;
  •     default:
  •       break;
  •   };
  •   res.send(req.path + queryString);
  • });

  • app.listen(port, () => console.log(`Example app listening on port ${port}!`))


코드가 깔끔하지는 않지만 원하는 기능에 대해 모두 반영을 해 보았다.
winston 공식 메뉴얼을 이용해서 가능한 deprecated 된 기능이 없도록 구성하였다.


[참고 사이트]


콘솔, 파일 혹은 데이터베이스로 로그를 남기는 방법에 대해 설명한다.
리모트 프로세스의 로그를 통합하는데 필요한 기능을 설명한다.
https://mcpaint.tistory.com/198

주로 참고한 사이트로 날짜표시, 파일이름 표시방법을 설명한다.
https://www.digitalocean.com/community/tutorials/how-to-use-winston-to-log-node-js-applications

깔끔한 로그 만들기, 화면과 파일에 2중으로 로그남기기, 날짜별로 다른 이름 지정을 한다.
https://thisdavej.com/using-winston-a-versatile-logging-library-for-node-js/

Rest API를 위한 Log 관리를 설명한다.
https://medium.com/aha-official/%EC%95%84%ED%95%98-rest-api-%EC%84%9C%EB%B2%84-%EA%B0%9C%EB%B0%9C-13-b90f6007a8f9

morgan의 Stream을 winston의 log파일에 사용한다.
https://stackoverflow.com/questions/27906551/node-js-logging-use-morgan-and-winston

process.env 사용에 대해 설명한다.
https://codeburst.io/process-env-what-it-is-and-why-when-how-to-use-it-effectively-505d0b2831e7

2019년 3월 3일 일요일

Node.js 보안 및 Https 서버 구성

Node.js Https 서버 구성


1. 암호화 해시 알고리즘


암호화 해시 알고리즘은 다음과 같다.

  • MD5는 보안용도로 권장하지 않음
  • SHA(secure hash algorithm)  사용


md5 - q 파일 이름 => md5 해쉬값을 비교가능 (파일 다운로드 변조 확인)

Hash값에 Salt (임의 문자를 추가)해서 해킹에 대한 방지

대칭 알고리즘의 종류는 다음과 같다.

  • DES (Data Encryption Standard)는 보안에 취약
  • AES(Advanced Encryption Standard)로 대체
대칭 암호화 : crypto. cipher/Decipher 사용


비대칭 알고리즘은 개인키와 공개키를 사용하여 비대칭으로 암호화 한다.


사설 인증서는 openssl이 있고 사용 시 경고가 발생한다.
openssl을 설치한다. 다음 명령을 이용하여 인증서를 발급한다.

  • openssl genrsa -out key.pem 2048 (Private Key 생성)
  • openssl req -new -key key.pem -out req.csr (인증서 발급 요청)
  • openssl x509 -req -in req.csr -signkey key.pem -out cert.pem -days 365 (인증서 발급 승인) 

두번째 명령어 실행 중 openssl.cnf파일을 찾을 수 없다는 오류가 발생하면 해당 파일의 위치를 명령어에 지정해야 한다.
C:\Users\jungwonkun7\내 프로그램\openssl-0.9.8k_WIN32\bin>openssl req -config ..
/openssl.cnf -new -key key.pem -out req.csr
Loading 'screen' into random state - done
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:KR
State or Province Name (full name) [Some-State]:SEOUL
Locality Name (eg, city) []:Seoul
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Stormboy
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:WK
Email Address []:stormboy@hanmail.net

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:



영리 목적으로 하는 개인 정보 수집 서비스의 경우는 보안 서버(https)가 의무화 되어 있다.
무료로 인증을 제공하는 사이트는 다음과 같다.

  • https://www.startssl.com
  • https://letsencrypt.org

2. https 서버 프로그램 작성

SSL 인증서와 함께 다음 코드를 작성한 후 node로 실행한다.

  • let http=require('http'),
  •     https = require('https'),
  •     express = require('express'),
  •     fs = require('fs');

  • let options = {
  •     key: fs.readFileSync('./key.pem'),
  •     cert: fs.readFileSync('./cert.pem')
  • };


  • let port1 = 80;
  • let port2 = 443;

  • let app = express();
  • app.use(express.urlencoded({extended: false}));

  • http.createServer(app).listen(port1, function(){
  •   console.log("Http server listening on port " + port1);
  • });


  • https.createServer(options, app).listen(port2, function(){
  •   console.log("Https server listening on port " + port2);
  • });

  • app.get('/', function (req, res) {
  •     res.writeHead(200, {'Content-Type' : 'text/html'});
  •     res.write('<h3>Welcome</h3>');
  •     res.write('<a href="/login">Please login</a>');
  •     res.end();
  • });

  • app.get('/login', function (req, res){
  •     res.writeHead(200, {'Content-Type': 'text/html'});
  •     res.write('<h3>Login</h3>');
  •     res.write('<form method="POST" action="/login">');
  •     res.write('<label name="userId">UserId : </label>')
  •     res.write('<input type="text" name="userId"><br/>');
  •     res.write('<label name="password">Password : </label>')
  •     res.write('<input type="password" name="password"><br/>');
  •     res.write('<input type="submit" name="login" value="Login">');
  •     res.write('</form>');
  •     res.end();
  • })

  • app.post('/login', function (req, res){
  •     let userId = req.params("userId");
  •     let password = req.params("password")

  •     res.writeHead(200, {'Content-Type': 'text/html'});
  •     res.write('Thank you, '+userId+', you are now logged in.');
  •     res.write('<p><a href="/"> back home</a>');
  •     res.end();
  • });
https://localhost/login 과 http://localhost/login을 접속하여 각각 실행되는 결과를 확인한다.

[참고 사이트]

https://www.youtube.com/watch?v=gks3CbpgtmI (Node 보안 강의)
http://blog.saltfactory.net/implements-nodejs-based-https-server/ (openssl과 node.js로 https서버 만들기)
https://code.google.com/archive/p/openssl-for-windows/downloads (window용 openssl설치파일)

Node.js & OAuth2 사용하여 Server 기능 구성

Node.js & OAuth2 사용하여 Server 기능 구성


1. OAuth2 란?

3자 인증 - 다른 서비스에서 토큰 발급

인증 처리
사용자 정보 암호화
보안 프로토콜의 사용

인증모듈 : passport, everyauth

authenticate
passport.serializeUser
passport.deserializeUser
req.user 사용자 정보

Local Auth
passport-local 모듈
var LocalStrategy = new LocalStrategy(Option, function(username, password, done) {})
passReqToCallback

done(null, USER-INFO)
done(null, false, FAILURE-INFO)


2. express용 OAuth Server 모듈 설치 및 테스트

express용 OAuth server 모듈을 설치한다.  OAuth2-Server 모듈을 express용으로 래핑한 것으로 express 서버에서 동작한다.

  • npm install express-oauth-server

설치 후 credential을 제공하는 방식에 따라 model에 getAccessToken등을 지정한다.
model에 OAuth에서 사용하는 명령어에 따라 작동 방식이 지정된다.
model에 대한 예제는 Github를 통해 확인할 수 있다.

3. Passport로 Local Auth 만들기

Local Auth를 만들기 위해서는 passport, passport-local 모듈을 설치해야 한다.


  • PS C:\ubuntu\node\oauth2svr\auth2> npm install express body-parser express-session passport passport-local --save

설치가 완료되면 다음과 같은 내용으로 node 프로그램을 작성한다.  프로그램 소스는 T Academy 동영상을 참조하였다. (90% 동일)

  • let express = require('express');
  • let bodyParser = require('body-parser');
  • let app = express();
  • app.use(bodyParser.urlencoded({extended:false}));

  • let session = require('express-session');
  • app.use(session({
  •   resave:false,
  •   saveUninitialized:false,
  •   secret:'Secret Key'
  • }));

  • let passport = require('passport');
  • app.use(passport.initialize());
  • app.use(passport.session());

  • let LocalStrategy = require('passport-local').Strategy;
  • let strategy = new LocalStrategy(function(username, password, done) {
  •   if (username==='user' && password === '1234') {
  •     let userinfo = {name:'사용자', email:"user@email.com"};
  •     done(null, userinfo);
  •   } else {
  •     done(null, false, '로그인 실패');
  •   }
  • });
  • passport.use(strategy);

  • passport.serializeUser(function(user, done) {
  •   console.log('세션에 기록하기');
  •   done(null, user);
  • });

  • passport.deserializeUser(function(user, done) {
  •   console.log('세션에서 사용자 정보 읽기');
  •   done(null, user);
  • });

  • app.post('/login', passport.authenticate('local'), (req,res) => { res.send('success')});
  • app.get('/personal', showPersonal);
  • app.listen(3000);

  • function showPersonal(req, res) {
  •   let user = req.user;
  •   if ( user ) {
  •     res.send('Personal Page ' + user.name + ' Email: ' + user.email);
  •   } else {
  •     res.sendStatus(401);
  •   }
  • }

node로 프로그램을 실행하면 아무런 메시지가 표시되지 않는다.  Postman을 이용하여 다음 사항을 테스트한다.

  • Get : Localhost:3000/personal 접속하여 결과 확인
  • Post: Localhost:3000/login username: user, password: 1234를 x-www-form-urlencorded 방식으로 접속하여 결과 확인





[참고 사이트]

https://velopert.com/2448 (Express.js 서버에서 JWT 기반 회원인증시스템 구현하기)
https://victorydntmd.tistory.com/116 (JsonWebToken 모듈 사용 JWT 기반 회원인증 구현하기)
https://nesoy.github.io/articles/2017-04/NodejsAuthentication (Node 인증 시스템 구현 Passport)
https://behonestar.tistory.com/37 (Token 기반 인증)
http://www.passportjs.org/docs/oauth2-api/ (Passport npm)
https://opentutorials.org/course/3387 (Node.js 쿠키와 인증)
http://scottksmith.com/blog/2014/07/02/beer-locker-building-a-restful-api-with-node-oauth2-server/ (Building a RESTful API with Node OAuth2  Server)
https://www.youtube.com/watch?v=Qn48RgkUuaA (Passport를 이용한 인증)
https://oauth2-server.readthedocs.io/en/latest/ (OAuth2 server 모듈 Document)
https://www.npmjs.com/package/express-oauth-server (express 모듈용 OAuth2 server 모듈)
https://blog.cloudboost.io/how-to-make-an-oauth-2-server-with-node-js-a6db02dc2ce7

Node.js & SQL Server 인덱스 적용 속도 비교

Node.js & SQL Server 인덱스 적용 속도 비교

Tedious Getting Start 마지막 페이지에는 테이블에 500만 row를 생성한 후 인덱스 적용 여부에 따른 속도를 비교하는 예제가 있다.  Microsoft SQL Server의 성능을 테스트 하기 위해 Dummy 데이터를 준비하고 인덱스 적용 여부에 따른 Query 성능을 측정하는 데 유용할 것으로 보인다.

1. SQL Server 데이터 준비하기

다음과 같은 명령을 이용하여 500만 row를 생성한다.

  • sqlcmd -S localhost -U stormboy -P ***** -d mydb01 -t 60000 -Q "WITH a AS (SELECT * FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) AS a(a))
  • SELECT TOP(5000000)
  • ROW_NUMBER() OVER (ORDER BY a.a) AS OrderItemId
  • ,a.a + b.a + c.a + d.a + e.a + f.a + g.a + h.a AS OrderId
  • ,a.a * 10 AS Price
  • ,CONCAT(a.a, N' ', b.a, N' ', c.a, N' ', d.a, N' ', e.a, N' ', f.a, N' ', g.a, N' ', h.a) AS ProductName
  • INTO Table_with_5M_rows
  • FROM a, a AS b, a AS c, a AS d, a AS e, a AS f, a AS g, a AS h;"

500만 row를 생성하는데 몇분이 소요된다.


2. 성능 테스트

성능 테스트를 사용하기 위해 node-uuid 모듈이 추가로 사용된다.  uuid는 RFC4122 UUIDs를 생성하는 도구이다. 
UUID는 Universally Unique Identifier의 약자로 URN이라고도 한다. UUID는 128 bits 길이의 시공간에서 단 한개의 ID를 보장하기 위해 사용된다.

  • npm install node-uuid

홈페이지의 소스 중 config항목은 deprecated 되었으므로 일부 변경하여 다음과 같이 코드를 생성한다.

  • var Connection = require('tedious').Connection;
  • var Request = require('tedious').Request;
  • var uuid = require('node-uuid');
  • var async = require('async');

  • var config = {
  •   server: "localhost",
  •   // If you're on Windows Azure, you will need this:
  •   options: {encrypt: false},
  •   authentication: {
  •     type: "default",
  •     options: {
  •       userName: "stormboy",
  •       password: "*****",
  •   }
  • }
  •  ,options: {
  •    debug: {
  •      packet: true,
  •      data: true,
  •      payload: true,
  •      token: false,
  •      log: true
  •    },
  •    database: 'mydb01',
  •    encrypt: false // for Azure users
  •  }
  • };

  • var connection = new Connection(config);
  • function exec(sql) {
  •     var timerName = "QueryTime";

  •     var request = new Request(sql, function(err) {
  •         if (err) {
  •             console.log(err);
  •         }
  •     });
  •     request.on('doneProc', function(rowCount, more, rows) {
  •         if(!more){
  •             console.timeEnd(timerName);
  •         }
  •     });
  •     request.on('row', function(columns) {
  •         columns.forEach(function(column) {
  •             console.log("Sum: " +  column.value);
  •         });
  •     });
  •         console.time(timerName);
  •     connection.execSql(request);
  • }
  • // Open connection and execute query
  • connection.on('connect', function(err) {
  •     async.waterfall([
  •         function(){
  •             exec('SELECT SUM(Price) FROM Table_with_5M_rows');
  •         },
  •     ]);
  • });


생성된 코드를 실행하면 데이터 베이스를 select한 결과와 처리 속도가 표시된다.

  • PS C:\ubuntu\node\oauth2svr\mssql> node perf01
  • Sum: 50000000
  • QueryTime: 1826.074ms


생성된 테이블에 conlumnstore index를 지정한다.

  • PS C:\ubuntu\node\oauth2svr\mssql> sqlcmd -S localhost -U stormboy -P dnjsrms -d mydb01 -Q "CREATE CLUSTERED COLUMNSTORE INDEX Columnstoreindex ON Table_with_5M_rows;"
  • 메시지 35315, 수준 16, 상태 1, 서버 JUNGWONKUN7-PC\SQLEXPRESS, 줄 1
  • 이 버전의 SQL Server에서는 columnstore 인덱스를 만들 수 없으므로 CREATE INDEX 문이 실패했습니다. 각 SQL Server 버전의 기능 지원에 대한 자세한 내용은 온라인 설명서를 참조하십시오.

SQL Server 2014 Express 에서는 Columnstore index를 지원하지 않는것으로 보인다.
하지만 2016 sp1 부터는 Express에서 지원하는 것으로 blog를 통해 확인할 수 있다.



[참고 사이트]

https://tools.ietf.org/html/rfc4122 (RFC4122 UUID 표준)
https://ko.wikipedia.org/wiki/%EB%B2%94%EC%9A%A9_%EA%B3%A0%EC%9C%A0_%EC%8B%9D%EB%B3%84%EC%9E%90 (범용 고유 식별자)
https://docs.microsoft.com/ko-kr/sql/relational-databases/indexes/columnstore-indexes-described?view=sql-server-2014 (Columnstore Index 가이드)
https://www.sqlshack.com/sql-server-2014-columnstore-index/ (Columnstore index를 사용한 성능개선)
https://m.blog.naver.com/PostView.nhn?blogId=gun0626&logNo=40192874161&proxyReferer=https%3A%2F%2Fwww.google.com%2F (2014 Clustered Columnstore Index 설명)


2019년 3월 2일 토요일

Node.js & SQL Server Tedious Getting Started 따라하기

Node.js & SQL Server Tedious Getting Started 따라하기


1. node에 SQL Server Tedious module 설치

npm을 사용하여 다음과 같이 실행한다.

  • npm init 
  • npm install tedious
  • npm install async

async는 비동기 자바스크립트를 동기식으로 지원하는 모듈이다.  Node의 기능을 async로 수행하도록 변환해 주는 역할을 한다.  기존 Node에서는 동기식 기법으로 setTimeout, callback, promise를 사용했다.  일부 사용하는 데 문제가 있다고 판단되었고 이를 async와 await로 해결한다.  Database처리는 동기식으로 진행해야 하므로 async 모듈을 사용해서 처리한다.


2. SQL Server 접속하기

SQL Server에 접속하기 위해서 다음과 같은 코드를 사용했다.  Tedious 샘플을 이용하여 deprecated 된 항목을 수정했다.

  • var Connection = require('tedious').Connection;
  • var Request = require('tedious').Request;
  • var TYPES = require('tedious').TYPES;

  • // Create connection to database
  • var config = {
  •   server: "localhost",
  •   // If you're on Windows Azure, you will need this:
  •   options: {encrypt: false},
  •   authentication: {
  •     type: "default",
  •     options: {
  •       userName: "stormboy",
  •       password: "*******",
  •   }
  • }
  • /*
  •  ,options: {
  •    debug: {
  •      packet: true,
  •      data: true,
  •      payload: true,
  •      token: false,
  •      log: true
  •    },
  •    database: 'DBName',
  •    encrypt: true // for Azure users
  •  }
  •  */
  • };

  • var connection = new Connection(config);

  • // Attempt to connect and execute queries if connection goes through
  • connection.on('connect', function(err) {
  •   if (err) {
  •     console.log(err);
  •   } else {
  •     console.log('Connected');
  •   }
  • });
명령을 실행하면 다음과 같은 결과가 표시된다.

  • PS C:\ubuntu\node\oauth2svr\mssql> node 01connect
  • Connected

3. sqlcmd를 사용하여 테이블과 테스트 데이터 생성하기 

sqlcmd 명령을 사용하기 위해 cmd를 실행한다.  sqlcmd를 이용하여 SQL Server를 접속하고 새로운 테이블과 테스트 데이터를 생성한다.


  • PS C:\ubuntu\node\oauth2svr\mssql> sqlcmd -S localhost -U stormboy -P ***** -d mydb01
  • 1> create table Employees (
  • 2> Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
  • 3> Name NVARCHAR(50),
  • 4> Location NVARCHAR(50)
  • 5> );
  • 6> go

다음은 생성된 테이블에 테스트 데이터를 추가한다.

  • 1> INSERT INTO Employees (Name, Locaton) VALUES
  • 2> (N'Jared',N'Australia'),
  • 3> (N'Nikita', N'India'),
  • 4> (N'Tom',N'Germany');
  • 5> go

입력된 데이터는 다음과 같이 확인한다.

  • 1> select * from Employees;
  • 2> go
  • Id          Name                                               Location
  • ----------- -------------------------------------------------- --------------------------------------------------
  •           1 Jared                                              Australia
  •           2 Nikita                                             India
  •           3 Tom                                                Germany

  • (3개 행 적용됨)

sqlcmd 접속 시 기본 데이터베이스를 지정했고 사용자를 일반 사용자로 지정했기 떄문에 쿼리문에 데이터베이스 지정을 하지 않았다.

4. node로 SQL Server와 CRUD 실행하기

SQL Server와 연결이 되면 다음으로 기본 CRUD를 테스트 한다.  연결 떄와 달리 connect 옵션으로 database를 지정해야 한다.


  • var Connection = require('tedious').Connection;
  • var Request = require('tedious').Request;
  • var TYPES = require('tedious').TYPES;
  • var async = require('async');

  • // Create connection to database
  • var config = {
  •   server: "localhost",
  •   // If you're on Windows Azure, you will need this:
  •   options: {encrypt: false},
  •   authentication: {
  •     type: "default",
  •     options: {
  •       userName: "stormboy",
  •       password: "******",
  •   }
  • }
  •  ,options: {
  •    debug: {
  •      packet: true,
  •      data: true,
  •      payload: true,
  •      token: false,
  •      log: true
  •    },
  •    database: 'mydb01',
  •    encrypt: false // for Azure users
  •  }

  • };

  • var connection = new Connection(config);

  • function Start(callback) {
  •     console.log('Starting...');
  •     callback(null, 'Jake', 'United States');
  • }

  • function Insert(name, location, callback) {
  •     console.log("Inserting '" + name + "' into Table...");

  •     request = new Request(
  •         'INSERT INTO Employees (Name, Location) OUTPUT INSERTED.Id VALUES (@Name, @Location);',
  •         function(err, rowCount, rows) {
  •         if (err) {
  •             callback(err);
  •         } else {
  •             console.log(rowCount + ' row(s) inserted');
  •             callback(null, 'Nikita', 'United States');
  •         }
  •         });
  •     request.addParameter('Name', TYPES.NVarChar, name);
  •     request.addParameter('Location', TYPES.NVarChar, location);

  •     // Execute SQL statement
  •     connection.execSql(request);
  • }

  • function Update(name, location, callback) {
  •     console.log("Updating Location to '" + location + "' for '" + name + "'...");

  •     // Update the employee record requested
  •     request = new Request(
  •     'UPDATE Employees SET Location=@Location WHERE Name = @Name;',
  •     function(err, rowCount, rows) {
  •         if (err) {
  •         callback(err);
  •         } else {
  •         console.log(rowCount + ' row(s) updated');
  •         callback(null, 'Jared');
  •         }
  •     });
  •     request.addParameter('Name', TYPES.NVarChar, name);
  •     request.addParameter('Location', TYPES.NVarChar, location);

  •     // Execute SQL statement
  •     connection.execSql(request);
  • }

  • function Delete(name, callback) {
  •     console.log("Deleting '" + name + "' from Table...");

  •     // Delete the employee record requested
  •     request = new Request(
  •         'DELETE FROM Employees WHERE Name = @Name;',
  •         function(err, rowCount, rows) {
  •         if (err) {
  •             callback(err);
  •         } else {
  •             console.log(rowCount + ' row(s) deleted');
  •             callback(null);
  •         }
  •         });
  •     request.addParameter('Name', TYPES.NVarChar, name);

  •     // Execute SQL statement
  •     connection.execSql(request);
  • }

  • function Read(callback) {
  •     console.log('Reading rows from the Table...');

  •     // Read all rows from table
  •     request = new Request(
  •     'SELECT Id, Name, Location FROM Employees;',
  •     function(err, rowCount, rows) {
  •     if (err) {
  •         callback(err); 
  •     } else {
  •         console.log(rowCount + ' row(s) returned');
  •         callback(null);
  •     }
  •     });

  •     // Print the rows read
  •     var result = "";
  •     request.on('row', function(columns) {
  •         columns.forEach(function(column) {
  •             if (column.value === null) {
  •                 console.log('NULL');
  •             } else {
  •                 result += column.value + " ";
  •             }
  •         });
  •         console.log(result);
  •         result = "";
  •     });

  •     // Execute SQL statement
  •     connection.execSql(request);
  • }

  • function Complete(err, result) {
  •     if (err) {
  •    //     callback(err); // 맨 마지막 callback function should be replaced to "console.error(err)"
  •            console.error(err);
  •     } else {
  •         console.log("Done!");
  •     }
  • }

  • // Attempt to connect and execute queries if connection goes through
  • connection.on('connect', function(err) {
  •   if (err) {
  •     console.log(err);
  •   } else {
  •     console.log('Connected');

  •     // Execute all functions in the array serially
  •     async.waterfall([
  •         Start,
  •         Insert,
  •         Update,
  •         Delete,
  •         Read
  •     ], Complete)
  •   }
  • });

node로 실행을 하면 다음과 같은 결과가 표시된다.

  • PS C:\ubuntu\node\oauth2svr\mssql> node 02connect
  • Connected
  • Starting...
  • Inserting 'Jake' into Table...
  • 1 row(s) inserted
  • Updating Location to 'United States' for 'Nikita'...
  • 1 row(s) updated
  • Deleting 'Jared' from Table...
  • 1 row(s) deleted
  • Reading rows from the Table...
  • 2 Nikita United States
  • 3 Tom Germany
  • 4 Jake United States
  • 3 row(s) returned

데이터베이스 Query 실행 중 오류가 발생하면 다음과 같은 엉뚱한 결과가 표시된다.

  • C:\ubuntu\node\oauth2svr\mssql\02connect.js:134
  •         callback(err);
  •         ^

  • ReferenceError: callback is not defined

왜 Callback 함수를 인식 못하는 건가?

확인해 보니 err를 처리하기 위해서는 맨 마지막으로 callback을 받는 함수는 callback(err)가 아닌 console.log(err)를 사용해야 한다.

그러므로 line 134는 console.log(err)로 변경해야 한다.

5. 더 나은 데이터베이스 조작 성능을 위한 Sequelize 사용

Sequelize.js는 Node.js 기반의 ORM(Object-Relational-Mapping)이다.  ORM을 사용하면 직접 데이터베이스를 조작하는 것보다 수배의 성능 향상을 가질 수 있다.  Sequelize가 지원하는 데이터베이스는 PostgreSQL, MySQL, MariaDB, SQLite, MS-SQL로 기본적인 데이터베이스를 모두 지원한다.

  • npm install sequelize
기본적으로 제공되는 소스에서 사용자ID, 암호, 그리고 데이터베이스를 지정하고 실행하면 테이블이 생성되고 테스트 데이터가 추가된다.

  • var Sequelize = require('sequelize');
  • var userName = 'stormboy';
  • var password = '*******'; // update me
  • var hostName = 'localhost';
  • var DbName = 'mydb01';

  • // Initialize Sequelize to connect to sample DB
  • var sampleDb = new Sequelize(DbName, userName, password, {
  •     dialect: 'mssql',
  •     host: hostName,
  •     port: 1433, // Default port
  •     logging: false, // disable logging; default: console.log

  •     dialectOptions: {
  •         requestTimeout: 30000 // timeout = 30 seconds
  •     }
  • });

  • // Define the 'User' model
  • var User = sampleDb.define('user', {
  •     firstName: Sequelize.STRING,
  •     lastName: Sequelize.STRING
  • });

  • // Define the 'Task' model
  • var Task = sampleDb.define('task', {
  •     title: Sequelize.STRING,
  •     dueDate: Sequelize.DATE,
  •     isComplete: Sequelize.BOOLEAN
  • });

  • // Model a 1:Many relationship between User and Task
  • User.hasMany(Task);

  • console.log('**Node CRUD sample with Sequelize and MSSQL **');

  • // Tell Sequelize to DROP and CREATE tables and relationships in the database
  • sampleDb.sync({force: true})
  • .then(function() {
  •     console.log('\nCreated database schema from model.');

  •     // Create demo: Create a User instance and save it to the database
  •     User.create({firstName: 'Anna', lastName: 'Shrestinian'})
  •     .then(function(user) {
  •         console.log('\nCreated User:', user.get({ plain: true}));

  •         // Create demo: Create a Task instance and save it to the database
  •         Task.create({
  •             title: 'Ship Helsinki', dueDate: new Date(2017,04,01), isComplete: false
  •         })
  •         .then(function(task) {
  •             console.log('\nCreated Task:', task.get({ plain: true}));

  •             // Association demo: Assign task to user
  •             user.setTasks([task])
  •             .then(function() {
  •                 console.log('\nAssigned task \''
  •             + task.title
  •             + '\' to user ' + user.firstName
  •             + ' ' + user.lastName);

  •                 // Read demo: find incomplete tasks assigned to user 'Anna''
  •                 User.findAll({
  •                     where: { firstName: 'Anna'},
  •                     include: [{
  •                         model: Task,
  •                         where: { isComplete: false }
  •                     }]
  •                 })
  •                 .then(function(users) {
  •                     console.log('\nIncomplete tasks assigned to Anna:\n',
  •                 JSON.stringify(users));

  •                     // Update demo: change the 'dueDate' of a task
  •                     // findById를 findByPk로 변경하여 사용
  •                     Task.findByPk(1).then(function(task) {
  •                         console.log('\nUpdating task:',
  •                 task.title + ' ' + task.dueDate);
  •                         task.update({
  •                             dueDate: new Date(2016,06,30)
  •                         })
  •                         .then(function() {
  •                             console.log('dueDate changed:',
  •                     task.title + ' ' + task.dueDate);

  •                             // Delete demo: delete all tasks with a dueDate in 2016
  •                             console.log('\nDeleting all tasks with with a dueDate in 2016');
  •                             Task.destroy({
  •                                 where: { dueDate: {$lte: new Date(2016,12,31)}}
  •                             })
  •                             .then(function() {
  •                                 Task.findAll()
  •                                 .then(function(tasks) {
  •                                     console.log('Tasks in database after delete:',
  •                         JSON.stringify(tasks));
  •                                     console.log('\nAll done!');
  •                                 })
  •                             })
  •                         })
  •                     })
  •                 })
  •             })
  •         })
  •     })
  • })

node로 소스를 실행하면 다음과 같은 결과가 표시된다.
  • PS C:\ubuntu\node\oauth2svr\mssql> node 03connect
  • sequelize deprecated String based operators are now deprecated. Please use Symbol based operators for better security, read more at http://docs.sequelizejs.com/manual/tutorial/querying.html#operators ..\node_modules\sequelize\lib\sequelize.js:242:13
  • **Node CRUD sample with Sequelize and MSSQL **
  • tedious deprecated The "config.userName" property is deprecated and future tedious versions will no longer support it. Please switch to using the new "config.authentication" property instead. ..\node_modules\sequelize\lib\dialects\mssql\connection-manager.js:70:26
  • tedious deprecated The "config.password" property is deprecated and future tedious versions will no longer support it. Please switch to using the new "config.authentication" property instead. ..\node_modules\sequelize\lib\dialects\mssql\connection-manager.js:70:26
  • tedious deprecated The default value for `options.encrypt` will change from `false` to `true`. Please pass `false` explicitly if you want to retain current behaviour. ..\node_modules\sequelize\lib\dialects\mssql\connection-manager.js:70:26

  • Created database schema from model.

  • Created User: { id: 1,
  •   firstName: 'Anna',
  •   lastName: 'Shrestinian',
  •   updatedAt: 2019-03-03T06:41:08.857Z,
  •   createdAt: 2019-03-03T06:41:08.857Z }


  • Created Task: { id: 1,
  •   title: 'Ship Helsinki',
  •   dueDate: 2017-04-30T15:00:00.000Z,
  •   isComplete: false,
  •   updatedAt: 2019-03-03T06:41:08.919Z,
  •   createdAt: 2019-03-03T06:41:08.919Z }

  • Assigned task 'Ship Helsinki' to user Anna Shrestinian

  • Incomplete tasks assigned to Anna:
  •  [{"id":1,"firstName":"Anna","lastName":"Shrestinian","createdAt":"2019-03-03T06:41:08.857Z","updatedAt":"2019-03-03T06:41:08.857Z","tasks":[{"id":1,"title":"Ship Helsinki","dueDate":"2017-04-30T15:00:00.000Z","isComplete":false,"createdAt":"2019-03-3T06:41:08.919Z","updatedAt":"2019-03-03T06:41:09.025Z","userId":1}]}]
  • sequelize deprecated Model.findById has been deprecated, please use Model.findByPk instead ..\node_modules\sequelize\lib\model.js:4208:9

  • Updating task: Ship Helsinki Mon May 01 2017 00:00:00 GMT+0900 (GMT+09:00)
  • dueDate changed: Ship Helsinki Sat Jul 30 2016 00:00:00 GMT+0900 (GMT+09:00)

  • Deleting all tasks with with a dueDate in 2016
  • Tasks in database after delete: []

  • All done!
홈페이지의 소스를 실행하면 Sequelize에서 2개 메소드, Tedious애서 3개 메소드가 deprecated 되었음을 확인할 수 있다. 
Sequelize는 연결하는 데이터베이스 종류에 따라 자동으로 Tedious모듈을 import하는 것으로 보인다.
Tedious모듈이 deprecated 된 메소드는 Sequelize가 업데이트 되어야 해결할 수 있다.

// blog를 확인해 보면 해당 문제는 v6에서 해결을 준비하는 것으로 보인다.

findById를 findByPk로 변경한 후 실행하면 첫번째 발생했던 symbol base operator 문제도 같이 해결되는 것을 확인할 수 있다.

[참고 사이트]

https://www.microsoft.com/en-us/sql-server/developer-get-started/node/windows/step/2.html (Create Node.js Application with SQL Server)
https://blueshw.github.io/2018/02/27/async-await/ (aysnc, await를 사용한 비동기 Javascript 동기식 만들기)
https://hyunseob.github.io/2016/03/27/usage-of-sequelize-js/ (Sequelize.js 다루기)
http://docs.sequelizejs.com/manual/installation/getting-started.html (Sequelize 공식사이트)