2018년 4월 23일 월요일

WPF - 달력 컨트롤 만들기

오랜만에 사용자 컨트롤을 하나 만드려다 보니 기억이 잘 안났다.
가지고 있는 책도 없고, 다시 한번 정리해야 했다.

System.Object
  System.Windows.Threading.Dispatcher
  ㄴ쓰레드에 대한 작업 항목 큐를 관리하기 위한 서비스를 제공함.
  System.Windows.Threading.DispatcherObject
  ㄴDispatcher와 연관된 개체 요소.
    System.Windows.DependencyObject
    ㄴDependency property 시스템에 참여하는 개체를 나타낸다.
      System.Windows.Media.Visual
      ㄴHit test, 좌표 변형 및 경계 계산 등의 WPF 렌더링을 제공하는 요소.
        System.Windows.UIElement
        ㄴ 외관을 갖고 입력 처리를 할 수 있는 개체에 대한 기본 클래스 (Layout + Input + Focus + Events)
          System.Windows.FrameworkElement
          UIElement + Data Binding + Data Template + Styles, ...
            System.Windows.Controls.Control
            ㄴ User Interface의 기본 클래스. ControlTemplate으로 외관을 정의한다. (FrameworkElement + Control Template + Background / Foreground + Font, ...)
              System.Windows.Controls.ContentControl
              ㄴ모든 유형의 컨텐츠를 하나의 Control 개체로 표현하는데 필요한 기능들이 구현된 클래스.
                System.Windows.Controls.UserControl
                ㄴ컨트롤을 만드는 간단한 방법을 제공하기 위한 클래스.
            System.Windows.Controls.ContentPresenter
            ㄴContentControl의 내용을 표시하기 위한 클래스.

WPF의 UI 요소에 대한 클래스 계층은 잘 정리되어 있긴 하지만 위처럼 꽤 복잡한 편이다. 완전히 새로운 것이 아니라 기존의 컨트롤을 조합해서 신규 컨트롤을 만드는 관점에서는 Control 또는 UserControl 레벨에서 출발하는 것이 좋을 것이다. 또는 ContentPresenter가 쓰일 수 있다.

어쨌든 달력을 만들어야 했다. WPF엔 DatePicker가 있긴 하지만 구글 캘린더와 같이 하나의 컨트롤에서 특정 날짜의 해야할 목록을 자유롭게 편집할 수 있는 컨트롤이 필요했다.

시작점은 ItemsControl. 반복된 컨텐츠를 어떤 틀 안에 담고자 할땐 ItemsControl이 좋다. 각각의 항목을 표현하기 위한 ItemTemplate과 여러개의 항목을 담기 위한 ItemsPanel만 정의해주면 된다.

<ItemsControl.ItemTemplate>
  <DataTemplate>
...

<ItemsControl.ItemsPanel>
  <ItemsPanelTemplate>
...

그리고 달력같이 정해진 규격 내에서의 반복이라면 패널 중 UniformGrid가 적합하다. 달력의 세로는 6행, 가로는 일월화수목금의 7열.
<ItemsControl.ItemsPanel>
  <ItemsPanelTemplate>
    <UniformGrid Rows="6" Columns="7"/>
...

ItemsTemplate엔 DockPanel을 이용해 날짜를 표시하기 위한 TextBlock을 Top에 두고, AcceptsReturn 속성을 True로 갖는 TextBox를 하나 놓는다. 그리고나서 오늘을 나타내는 속성, 주말을 나타내는 속성, 이번달을 나타내는 속성 등을 지정해 화면을 치장하면 된다.

몰랐었는데 UserControl 대신 Control + Style 조합으로 컨트롤을 만드는게 더 간편하더라. 뷰에 대한 클래스는 UserControl이 아니라 Control을 상속받아 구현하게 되는데 반드시 static 생성자에서 OverrideMetadata를 호출해 주어야 한다. 그리고나서 필요한 DependencyProperty와 Style을 정의한다.
<Style TargetType="{x:Type ... }">
  <Setter Property="Template">
    <ControlTemplate>
...


WPF를 사용하는 사람이 최근엔 거의 없겠지만 쓰면 쓸수록 WPF는 참 잘만들어진 UI 프레임워크라 생각한다. 그런데 진짜 쓰는 사람 없는듯.

2018년 4월 17일 화요일

Node.js - 스터디 4주차 - 정리 정돈의 기술

https://opentutorials.org/course/2136/
생활코딩 강의를 이용해 진행하는 스터디 3+@주차.

4/17(화) :: 37강 'jade - extends' 부터 43강 '글작성 + 인증 (CRUD + Auth)-OrientDB 2' 까지.


완강!!


jade - extends


37강 jade - extends 부터 마지막 강의 까지는 '정리정돈의 기술'이라는 테마로 진행된다. 우선 jade 템플릿 엔진을 사용할 때 중복을 제거할 수 있는 기법이 소개된다. python의 django에서도 나오는 extends ~ block 키워드를 이용하는 방법인데 일종의 템플릿 간 부모-자식 관계를 둬서 중복을 제거하는 기법이다. view에 대한 중복을 제거한다고 해서 성능이 향상되는 것은 아니겠지만 유지보수 등 관리 측면에서 이점이 있기 때문에 공통부는 따로 떼서 관리하는것이 낫다.

1) view.jade와 add.jade가 있는데 공통된 부분이 있다면 따로 떼어내 layout.jade란 파일로 옮겨적는다. 물론 layout.jade란 이름은 임의로 지은 것이며 자유롭게 지정할 수 있다.

2) view.jade, add.jade에서 공통된 부분을 삭제하고 extends ./layout이라 써준다. './layout'은 같은 폴더의 layout.jade를 가리킨다. 만약 layout.jade 파일명이 parent.jade라면 extends ./parent가 된다.

2) 공통 코드가 있는 layout.jade에 block 키워드로 자식 컨텐츠가 들어갈 위치를 지정한다. block의 이름은 예약된 키워드가 아니라면 자유롭게 지정할 수 있다. 예제에서 설명된 layout.jade의 내용은 다음과 같다.
doctype html
html
  head
  body
    ul
      li JavaScript
      li nodejs
      li expressjs
    article
      block content

3) view.jade, add.jade에서 공통된 부분을 삭제하고 extends ./layout이라 써준다. './layout'은 같은 폴더의 layout.jade를 가리킨다. 만약 layout.jade 파일명이 parent.jade라면 extends ./parent가 된다. 그리고 부모가 지정한 block 이름에 대한 내용을 각각 작성하면 된다. 부모가 'block child'라 이름지었다면 자식 view에서도 동일하게 'block child'라 작성해야 전체 컨텐츠가 올바르게 렌더링되는 것을 확인할 수 있다.
extends ./layout
block content
  form
    input(type='text')
    input(type='submit')

유사한 기능으로 include가 있다. include는 특정 jade의 내용을 삽입하는 기능이다. 자주 인용되는 head, header, footer 같은 내용은 extends 구조로 표현하기 보다 include로 처리하는 것이 구조상 낫다고 본다.

사용자 정의 모듈 만들기


자주 참조되는 기능 단위를 모듈로 정의할 수 있다. Node.js에서 모듈은 프로그램 복잡도를 줄이고자 하는 목적이 가장 크겠지만 의미있는 기능 단위를 다른 사람에게 배포할 때도 유용하게 쓰이는 개념이다. 모듈을 정의 방식도 매우 편리한데 제공하고자 하는 대상을 단순히 module.exports로 지정해주면 외부 파일 또는 또다른 모듈에서 이를 참조해 사용하거나 확장할 수 있다.

calculator.js
module.exports.sum = function(a, b) {
  return a + b;
}
module.exports.avg = function(a, b) {
  return (a + b) / 2;
}

example.js
var calc = require('./lib/calculator');
console.log(calc.sum(1, 2));
console.log(calc.avg(1, 2));

라우트 분리하기

모듈 시스템과 비슷한 방식으로 라우트 또한 정리가 가능하다. 복잡한 애플리케이션일수록 애플리케이션의 구조를 더 단순화시킬 필요가 있다. 극단적인 예로 1000개의 라우트를 정의해야 한다면 이를 하나의 파일에서 관리하는 것은 매우 어려울 것이다. 따라서 서로 연관된, 유사한 목적에 따른 라우트끼리 모아 분리된 파일로 관리할 필요가 있다.

라우트를 분리하기 위한 방법은 여러가지가 있는데 express에서 관련된 기능이 제공된다. 바로 Router-level 미들웨어를 사용하면 된다.
var route = express.Router();
route.get('/r1', function(req, res) {
  res.send('Hello /p1/r1');
});
route.get('/r2', function(req, res) {
  res.send('Hello /p1/r2');
});
app.use('/p1', route); // route 변수는 /p1/r1, /p1/r2 URI 처리를 담당하게 된다.

기존 application level express 미들웨어에 모든 라우트 기능을 지정했다면 router level express 미들웨어 선언을 통해 라우트 기능의 일부를 떼낼 수 있다. 위 예제에서와 같이 선언된 변수 route가 처리하는 내용을 별도 파일로 분리해 모듈로 지정하는 방식인데, 기능 단위로 잘 구분해서 라우트를 분할하면 전체 애플리케이션을 보다 편하게 관리할 수 있다.

글작성 + 인증 (CRUD + Auth) - MySQL 1, 2 / OrientDB 1, 2


이전 강의에서 MySQL 버전과 OrientDB 버전으로 작성했던 글작성 애플리케이션과 인증 애플리케이션을 깔끔한 구조로 정리하고 결합하는 것에 대한 강의다.

선 js에 박혀있던 html 내용을 jade로 이동시킨다. 변경할 땐 html2jade.org라는 웹사이트를 이용하면 직접 jade 구문을 작성하지 않고 바로 변환된 결과를 얻을 수 있다. jade의 extends, include 기능을 활용해 공통 부분을 별도 jade 파일로 나누도록 한다.

인증 기능에 대한 내용도 별도의 파일로 분할한다. 인증 기능에 대한 내용을 auth.js로 이동한 뒤 passport를 인자로 받는 모듈 함수를 노출시킨다. 함수 내부엔 router 레벨 미들웨어가 하나 있어서 인증과 관련된 요청에 대한 처리 작업을 수행한다.
module.exports = function (passport) {
    var route = require('express').Router();
    route.post( ...
    ...
}

메인 로직에선 auth.js로 분리한 모듈을 불러와 '/auth/' 하위 요청에 대한 처리를 담당하게끔 위임한다.
var auth = require('./routes/mysql/auth')(passport);
app.use('/auth/', auth);

위와 같은 패턴으로 나머지 기능 단위에 대해서도 파일을 분리해나가면 되는데 강의 내용에 다소 억지스러운 부분이 있어서 크게 와닿진 않는다. 어쨌든 express가 제공하는 라우팅 기능은 좀더 알아둘 필요가 있는 것 같다. 아래 링크를 참고해보자.

2018년 4월 16일 월요일

MVVM Light Toolkit

MVVM Light Toolkit은 GalaSoft라는 곳(?)에서 만든 MVVM 아키텍쳐의 구현체다. Laurent Bugnion이라는 스위스 취리히에 사는 개발자가 만들었는데 현재는 Microsoft Azure 팀에서 근무중인듯 하다. MVVM은 Model-View-ViewModel의 View와 Model을 분리하기 위한 아키텍쳐 스타일로, Prism과 함께 WPF에서 가장 많이 쓰이는 패턴 중 하나다. WPF가 더이상 잘 쓰이진 않지만 ㅠㅠ. View와 Model이 분리된 구조로 개발을 진행하면 Model에 대한 테스트 그리고 디자이너간 협업 측면에서 여러 이점이 있다.

MVVM Light Toolkit을 이용하면 당연히 WPF 기반의 MVVM 애플리케이션을 빠르게 개발할 수 있다. 주요 클래스 개수도 많지 않기 때문에 어렵지 않게 적응이 가능하다. 설치는 Nuget Package Manager를 이용하는게 가장 간편한 방법이다.

MVVM Light Toolkit은 2개의 main DLL로 유지된다.
GalaSoft.MvvmLight.dll
GalaSoft.MvvmLight.Extras.dll

GalaSoft.MvvmLight.dll의 주요 클래스 목록이다.
ObservableObject - Base class for Model classes. It creates Model properties as observable.
ViewModelBase - Base class for ViewModel classes.
RelayCommand - A command. More about commands here.
WeakAction - Stores an System.Action without causing a hard reference to be created to the Action's owner. The owner can be garbage collected at any time.
WeakFunc - Stores an Func without causing a hard reference to be created to the Func's owner. The owner can be garbage collected at any time.
Messenger - Class for allowing objects to exchange messages.
MessageBase - Base class for all messages broadcasted by the Messenger.
DispatcherHelper - Helper class for dispatcher operations on the UI thread.

다음은 Galasoft.MvvmLight.Extras.dll의 주요 클래스 목록이다. 
EventToCommand - Used for bind any event of FrameworkElement to ICommand.
SimpleIOC - An IOC container for register and resolve instances.

ObservableObject을 상속받아 모델을 구현하면 변경점이 바로 View에 반영되는데 필요한 기반 기능을 얻을 수 있다.
ViewModelBase를 상속받아 뷰-모델을 구현하면 View 도는 다른 ViewModel과 상호작용하는데 필요한 기반 기능을 얻을 수 있다.
Messenger는 의존 관계를 두지 않고 ViewModel과 ViewModel 간 상호작용할 수 있는 방법을 제공하며,

SimpleIOC라는 IOC 컨테이너를 통해 Service Locator 패턴을 쉽게 구현할 수 있는 방법을 제공한다.
void Register<TMessage>(object recipient, object token, bool receiveDerivedMessagesToo, Action<TMessage> action);
void Send<TMessage>(TMessage message, object token);

Messenger와 SimpleIOC는 모두 클래스 간 의존성을 줄여주기 위한 장치다.
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
ServiceLocator.Current.GetInstance<MainViewModel>();

공식 사이트와 깃허브 주소는 아래와 같다.

Node.js - 스터디 4주차 - Auth (OrientDB / MySQL)

https://opentutorials.org/course/2136/
생활코딩 강의를 이용해 진행하는 스터디 4주차.

4/16(월) :: 34강 'Auth - OrientDB' 부터 35강 'Auth - MySQL' 까지.

Auth - OrientDB


참고로 OrientDB엔 OUser, ORestricted와 같은 사용자 관련 내장 인증 체계를 가지고 있는데 이를 이용하면 직접 사용자와 관련된 것을 구축하는데 소요되는 시간과 노력을 덜 수 있다고 한다. Storage 단에서 뭘 얼마나 더 편하게 해주는지 궁금하긴 하지만 일단 pass.

스키마 설정

Schema에 들어가서 user란 클래스를 만들자. 그리고 애플리케이션에서 사용하는 프로퍼티를 등록해주자. 애플리케이션에서 필수로 받아야 하는 authId, displayName 속성엔 Mandatory, Not Null를 부여한다. 그리고 user.authId를 UNIQUE 타입의 Index로 지정하자. Index를 생성해줌으로써 authId를 이용한 쿼리에 대한 검색 성능을 향상시킬 수 있다.

애플리케이션 세팅

세션에 대한 처리와 사용자 정보 읽기/쓰기 작업을 해주어야 한다. 우선 예전 강의에서처럼 session의 store 타입을 OrientoStorage로 지정해준다. 프로그램을 다시 시작하면 OrientDB의 Session 테이블에 정보가 자동으로 추가되는 것을 확인할 수 있다.

var OrientoStore = require('connect-oriento')(session);
var config = {
  session: {
    server: "host=localhost&port=2424&username=root&password=111111&db=02"
  }
}
app.use(session({
  secret: 'my-secret-key', // for encrypt session id.
  resave: false,
  saveUninitialized: true,
  store: new OrientoStore(config.session)
}));

그리고나서 사용자 정보를 읽고 쓰는 과정인 Register / Login을 구현해주어야 한다. OrientDB 사용을 위해 'orientjs' 모듈을 가져온 후, OrientDB 접속을 위한 객체를 생성하고, 사용 선언을 한다. 코드는 아래와 같다.

var OrientDB = require('orientjs');
var server = OrientDB({
  host:
  port:
  username:
  password:
});
var db = server.use('o2');

마지막으로 메모리 레벨에서 동작하던 users 관련 코드를 OrientDB 버전의 insert, select구문으로 변경해주면 된다.


Auth - MySQL


OrientDB 버전에서와 마찬가지로 사용자 정보를 관리할 테이블을 생성한다.

스키마 설정

create table users(
    id INT NOT NULL AUTO_INCREMENT,
    authId VARCHAR(50) NOT NULL,
    username VARCHAR(30),
    password VARCHAR(255),
    salt VARCHAR(255),
    displayName VARCHAR(50),
    PRIMARY KEY(id),
    UNIQUE(authId)
) ENGINE = InnoDB;

애플리케이션 세팅

session의 store 타입을 다시 mysql로 지정해준 후 애플리케이션을 재시작하면 MySQL이 sessions 테이블을 자동으로 생성하고 관련 세션 정보를 기록하는 것을 확인할 수 있다.

var MySQLStore = require('express-mysql-session')(session);
app.use(session({
  secret: 'my-secret-key', // for encrypt session id.
  resave: false,
  saveUninitialized: true,
  store: new MySQLStore({
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: '111111',
    database: 'o2'
  })
}));

사용자 정보를 읽고 쓰기 위해 mysql 모듈을 로드한다. Session 스토어는 단순히 브라우져 세션 데이터를 관리하기 위한 장치이고, 실제 사용자 등록과 로그인 과정은 MySQL과 passport.js를 이용해 직접 구현해주어야 한다. MySQL에 대한 초기화 코드는 아래와 같다.

var mysql = require('mysql');
var conn = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: '111111',
  database: 'o2'
});
conn.connect();

사용자 등록

앞선 강의에서 보았듯 MySQL 모듈에서 치환자는 '?'를 사용하는데 여기서 나아가 key-value 객체를 바로 쿼리에 할당할 수 있는 편의 기능이 지원된다.

app.post('/auth/register', function(req, res) {
  hasher({password: req.body.password}, function(err, pass, salt, hash) {
    var user = {
      authId: 'local:' + req.body.username,
      displayName: req.body.displayName,
      username: req.body.username,
      password: hash,
      salt: salt
    }
    var sql = 'INSERT INTO users SET ?';
    conn.query(sql, user, function(err, results) {

위 구문과 같이 치환자 '?' 하나로 원하는 파라미터를 넣어줄 수 있다. user 객체의 property는 db 스키마의 column 이름과 같아야 한다.

2018년 4월 12일 목요일

Node.js - 스터디 3주차 - 사용자 관리 (인증)

https://opentutorials.org/course/2136/
생활코딩 강의를 이용해 진행하는 스터디 3주차.

4/13(금) :: 30강 'Multi user(다중 사용자)' 부터 33강 'Federation Authentication(타사 인증)' 까지.


Multi user (다중 사용자)


회원 가입과 로그인을 구현한다. 강의에선 MySQL과 OrientDB를 모두 다루고 있기 때문에 우선 파일 시스템으로 애플리케이션을 구현하는 예를 보여주고 있다. 1) register form을 통해 입력되는 사용자 가입 정보를 global 배열에 집어넣고, 2) 로그인 요청이 있으면 global 배열에서 일치하는 정보가 있는지 확인한 후 welcome 페이지를 띄우는 예제다.

전반적인 흐름에 대한 내용을 파악하는 것이 이 강의의 목적이므로 엄격한 수준의 validation이나 중복 처리 같은 예외상황 처리는 하지 않는다.

배열을 선언하고,
var users = [];

사용자 정보를 추가한다.
var user = {
  username: req.body.username,
  password: req.body.password,
  displayName: req.body.displayName
};
users.push(user);

메모리에 임시적으로 기록되는 정보이기 때문에 서버가 꺼지면 정보는 모두 날아간다. 영속성을 보장하기 위해선 데이터베이스와 같은 별도의 스토리지가 필요한데 이러한 부분은 뒷 강의에서 나오는 것 같다.


Security - Password (비밀번호 보안)


사용자의 비밀번호가 노출되어 악의적인 목적을 가진 사람의 손으로 넘어간다면 큰 문제가 된다. 지속가능한 서비스를 위해 사용자의 비밀번호는 외부에 유출되어선 안되고, 최악의 상황에 대한 대비가 되어 있어야 한다. 최악의 상황에 대한 대비란 얘기는 사용자 비밀번호를 평문 형태로 스토리지에 보관하지 않는 것을 기본으로 한다.

일반적으로 사용자의 비밀번호는 암호화 알고리즘을 통해 암호화된 값으로 스토리지에 보관한다. 그리고 비밀번호 비교 과정에서도 사용자의 입력 비밀번호를 동일한 방식으로 암호화한 후, 결과 해시값 비교를 통해 유효성을 판단한다.

단방향 암호화 알고리즘인 sha256을 사용해보자.
npm install sha256

> var sha256 = require('sha256');
> sha256('javascript');
'eda71746c01c3f465ffd02b6da15a6518e6fbc8f06f1ac525be193be5507069d'

sha256을 이용해 'javascript'를 암호화하면 항상 위와 같은 결과를 얻게된다. 'javascript'를 sha256으로 암호화 했을 때의 결과 값이 'eda71746c01c3f465ffd02b6da15a6518e6fbc8f06f1ac525be193be5507069d'인 것을 알더라도 'eda71746c01c3f465ffd02b6da15a6518e6fbc8f06f1ac525be193be5507069d'만 보고 원문이 'javascript'라는 것을 알기는 매우 어렵다. 사용자가 최초 입력한 원문에 대해 보안을

단순히 암호화 알고리즘을 사용해 정보 보안을 제공하는 방법 말고도 다양한 응용 기법이 존재한다. 대표적으로 암호화된 해시 값을 다시 N번 암호화하는 key stretching, 평문에 임의의 문자열을 추가해서 암호화 하는 salting 등이 있다. (http://d2.naver.com/helloworld/318732 를 참고하자.)

salting은 비밀번호를 암호화하는데 양념을 치는 방법이다.
> var salt = 'asofih23ihaog14oih';
> var password = 'javascript';
> sha256(password + salt);

위와 같이 임의의 문자열을 암호화 과정에 포함시키면 결과 해시 값을 알기 더 어려워진다. 추가로 사용자마다 임의의 salt 값을 부여한다면 결과를 추론하기 더 어려워지기 때문에 더 향상된 수준의 보안을 제공할 수 있게 된다.

Node.js에선 pbkdf2-password란 모듈이 제공된다. 앞서 언급된 내용을 모듈로 구현해 제공하는 것인데 구현간 발생할 수 있는 실수를 상당부분 줄여준다.
npm install pbkdf2-password --save
(사용법: https://www.npmjs.com/package/pbkdf2-password)

! sha256 모듈과 다르게 암호화 결과를 callback으로 제공한다는 점에 주의가 필요하다. node.js는 비동기 시스템이므로 callback이 호출되기 전에 redirect가 동작하는 등의 의도치 않은 결과가 나타날 수 있다.

1) 사용 선언
var bkfd2Password = require("pbkdf2-password");
var hasher = bkfd2Password();

2) 사용 예 - 사용자 등록

3) 사용 예 - 사용자 로그인


Passportjs




최근의 대다수 웹 애플리케이션은 자체 인증 기능만을 제공하지 않고, 페이스북, 구글, 트위터와 같은 서비스에 이미 가입되어 있는 정보를 이용해 사용자를 인증시켜주기도 한다. OAuth란 표준이 있는 것으로 알고있는데 강의에선 Federation Authentication이라 표현하고 있다. 아무튼 passport.js는 사용자 인증과 관련된 다양한 기능을 쉽게 구현할 수 있도록 지원한다. 물론 자체 인증 기능만 사용할 것이라면 굳이 passport.js를 사용할 필요는 없다.

Passport.js를 설치하자.
npm install passport passport-local --save

passport-local를 설치하는 이유는 자체 인증 기능까지 passport.js가 제공하는 방식(style)으로 처리하기 위함이다. (http://www.passportjs.org/docs/username-password/)

우선 미들웨어 사용 선언을 한다. 주의할 점은 session 모듈의 초기화를 마친 다음에 passport.session()를 호출해 주어야 올바르게 동작한다.


passport를 사용하면 기존 인증 기능에 대한 처리는 passport 방식으로 다시 수정해 주어야 한다. passport에서 특정 로그인 방식은 'strategy'란 개념으로 정리된다. 그리고 자체 인증 방식은 'local'이라는 이름으로 표현된다. 기존 예제에선 app.post('/auth/login') 내부에서 로그인과 관련된 모든 과정을 처리했었지만, passport 버전에선 passport의 authenticate 메서드로 처리를 위임한다. 인자로 strategy 이름과 redirect 경로에 대한 정보 등을 받는다.

당연히 위와 같이 하기만하면 로그인이 될 순 없고, 'local' 로그인 전략에 대한 처리 메서드를 직접 구현해야 한다. LocalStrategy란 객체를 선언하면서 처리 메서드를 하나 지정해주는 것인데 건네받는 인자로 username, password, 그리고 로그인 성공 결과 정보를 받는 done이란 callback 함수가 있다.

username, password에 form에서 입력한 값이 바로 꽂히는게 신기했는데, form에서 id가 username, password로 지정된 경우에 한해서만 값을 찾을 수 있다. form에서 지정한 id가 다르다면

passport.use(new LocalStrategy({
    usernameField: 'email',
    passwordField: 'passwd'
  },
  function(username, password, done) {
    // ...
  }

));
와 같이 직접 지정해주어야 한다. 전체 모습은 아래와 같다.

done의 첫 번째 인자는 에러 정보, 두 번째 인자는 로그인에 성공한 사용자 객체 또는 false 값이다. 전체적인 chaining은 잘 모르겠지만 어쨌든 done에 전달한 사용자 객체는 serialize되어 세션에 기록된다. 이 부분 또한 사용자가 직접 구현해 주어야 한다. 구현해야할 것은 세션에 값을 기록할 때 id 값을 지정해주기 위한 serializeUser와 세션에 기록된 값을 참조하기 위한 과정인 deserializeUser의 두가지 함수인데 크게 복잡하진 않다. 아래와 같이 unique 성질을 갖는 property를 serializeUser에 넣어주면 deserializeUser에선 그 property를 이용해 원하는 세션 데이터를 찾을 수 있는 구조다.



이렇게 구현이 진행되는 과정에서 자연스레 'req.session.displayName'과 같이 참조했었던 기존 예제가 passport 스타일로 변경된다. 영향을 받는 '/welcome' 페이지와 '/auth/logout'의 변경은 다음과 같다.




Federation Authentication (타사 인증)


Facebook 예. (http://www.passportjs.org/docs/facebook/)
npm install passport-facebook

var passport = require('passport')
  , FacebookStrategy = require('passport-facebook').Strategy;

1) Facebook strategy 구현.
passport.use(new FacebookStrategy({
    clientID: FACEBOOK_APP_ID,
    clientSecret: FACEBOOK_APP_SECRET,
    callbackURL: "http://www.example.com/auth/facebook/callback"
  },
  function(accessToken, refreshToken, profile, done) {
    User.findOrCreate(..., function(err, user) {
      if (err) { return done(err); }
      done(null, user);
    });
  }
));
→ profile: Facebook에 등록된 User 정보가 담긴 객체

2) 인증 요청을 Facebook으로 redirect.
// Redirect the user to Facebook for authentication.  When complete,
// Facebook will redirect the user back to the application at
//     /auth/facebook/callback
app.get('/auth/facebook', passport.authenticate('facebook'));

3) 인증 결과에 대한 redirect.
// Facebook will redirect the user to this URL after approval.  Finish the
// authentication process by attempting to obtain an access token.  If
// access was granted, the user will be logged in.  Otherwise,
// authentication has failed.
app.get('/auth/facebook/callback',
  passport.authenticate('facebook', { successRedirect: '/welcome',
                                      failureRedirect: '/auth/login' }));

동작하는 순서는 2) → 1) → 3).

2018년 4월 11일 수요일

Node.js - 스터디 3주차 - Session

https://opentutorials.org/course/2136/
생활코딩 강의를 이용해 진행하는 스터디 3주차.

4/12(목) :: 27강 'Session 1' 부터 29강 'Session 3' 까지.



Session 1


기본적인 상태유지 기능을 갖고 있지 않은 HTTP 프로토콜을 보완하기 위한 장치로 cookie와 session이 존재한다. 우선 기본은 cookie고, session는 cookie 기술을 바탕으로 동작하는 확장 기능 쯤으로 이해해보자.


session은 cookie와 다르게 정보를 사용자 컴퓨터에 저장하지 않고 서버에 저장한다. 특정 클라이언트에 대한 session 정보는 서버가 할당하는 session id를 통해 식별된다. (클라이언트(브라우저)는 서버로부터 session id를 전달받은 후 매 요청마다 session id를 헤더에 포함시키게 된다.) 결과적으로 클라이언트와 서버 간에는 session id만을 가지고 통신하기 때문에 session에 기록된 정보는 request, response 헤더에 드러나지 않는다. 따라서 request, response 헤더에 정보를 노출하는 cookie 방식에 비해 session이 보안상 안전하다고 말할 수 있다. 용량 제약이 큰 cookie에 비해 많은 양의 데이터를 session에 기록할 수 있다는 부분도 session이 가지는 장점이다.


하지만 session이 보안상, 공간상 장점이 있다고 해서 상태유지에 필요한 모든 사용자 데이터를 session에 저장하는 것은 옳지 않다. 서버의 성능에 무리를 줄 수 있기 때문이다. 따라서 보안에 신경쓰지 않아도 되는 가벼운 정보는 cookie로, 보안에 신경써주어야 하는 정보는 session으로 분리해서 관리하는 것이 옳다. 추가로 다른 프로그램과의 연동이 필요한 경우에 선택적으로 cookie를 활용할 수 있다.

cookie - 보안에 신경쓰지 않아도 되는 정보. (예 : '오늘 다시 알리지 않음'과 같은 팝업, 장바구니 목록 등.)

session - 보안에 신경써주어야 하는 정보. (예 : 사용자의 신원 데이터, 사용자의 로그인(접속) 정보 등.)

session 모듈을 설치하자.
npm install express-session --save

express-session 모듈의 경우 기본적으로 세션 정보를 메모리에 저장하므로 애플리케이션이 종료되면 모든 정보가 날아간다. 물론 데이터베이스를 이용해 세션 정보를 관리할 수도 있다.


다음은 간단한 테스트 앱이다. 일반 모드의 크롬 브라우저와 시크릿 모드의 크롬 부라우저를 띄운 뒤 session에 기록한 count 값이 어떻게 출력되는지 확인해보자.


var express = require('express');

var session = require('express-session');
var app = express();

app.use(session({

  secret: 'my-secret-key', // 이 키를 이용해 session id를 암호화 한다.
  resave: false, // 요청 중 session이 수정되지 않은 경우에도 session이 저장소에 다시 저장되도록 하는 옵션.
  saveUninitialized: true // 초기화되지 않은 session을 저장소에 저장하는 옵션. 잘 모르겠다.
}));

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

  if (req.session.count) {
    req.session.count++;
  } else {
    req.session.count = 1;
  }
  res.send('hi session: ' + req.session.count);
});

* 위 예제에서 보듯 req.session 객체에 원하는 값을 기록할 수 있다.

req.session.count = 1;

정보의 삭제는 'delete' 키워드를 이용해 프로퍼티 자체를 지우면 된다. 

delete req.session.count;


Session 2


session을 이용한 Login 앱 만들기. session을 이용하면 특정 사용자가 로그인 했을 때 로그인 정보를 애플리케이션 내에서 접근/관리할 수 있다. 단순하게 생각하는 편이 나은데 그냥 서버의 메모리에 기억하고 싶은 값을 기록하는 것 뿐이다. 이 값은 클라이언트에게 할당한 session id로 식별되고 변수 이름으로 참조된다.


1) 로그인 폼을 만들어 화면에 표시한다.


2) 로그인 요청에 대한 처리 기능을 만든다. 유효한 로그인이면 필요한 정보를 session에 기록하고 welcome 페이지로 전환한다.


3) welcome 페이지는 session 정보를 참조해서 화면을 표시한다.


4) 로그아웃을 요청하면 session 정보를 삭제한다.



Session 3


session 데이터를 메모리에 보관하는 방법은 실제 서비스에선 잘 사용하지 않는 방법이다. npm 모듈 페이지에서도 아래와 같이 안내되고 있다.

Warning: The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing.

강의에선 파일, MySQL, OrientDB 각각의 저장소를 이용해 session 데이터를 관리하는 방법을 알려주고 있다.


1) 파일

npm install session-file-store --save

var session = require('express-session');
var FileStore = require('session-file-store')(session);

app.use(session({
  ...
  store: new FileStore({ path: './sessions/' })
  ...
}));


2) MySQL

npm install express-mysql-session --save

var session = require('express-session');
var MySQLStore = require('express-mysql-session')(session);

app.use(session({
  ...
  store: new MySQLStore({
    host: 'localhost',
    port: 3306,
    user: 'root',
    password: '111111',
    database: 'o2'
  })
}));
→ MySQL 접속 정보를 같이 지정해준다. express-mysq-session 모듈은 전달받은 DB 접속 경로에 sessions라는 테이블을 생성하고, session 데이터에 대한 CRUD를 대신 수행해준다.




! 한가지 주의할 점이 있다. session 정보를 변경한 후 redirect를 호출하면 redirect된 화면에서 최신 session 정보를 읽어오지 못할 때가 있다. session 데이터 수정 후에 redirect가 필요하면 우선 save를 호출한 후 callback에서 redirect를 요청하도록 해주자.

req.session.save(function() {
  res.redirect('/welcome');
});


3) OrientDB

OrientDB를 session 저장소로 사용하는 방법은 MySQL 버전과 매우 유사하다.
npm install connect-oriento --save

사용법: https://www.npmjs.com/package/connect-oriento

2018년 4월 9일 월요일

Node.js - 스터디 3주차 - MySQL, Cookie

https://opentutorials.org/course/2136/

생활코딩 강의를 이용해 진행하는 스터디 3주차.

4/9(월) :: 22강 'MySQL 소개 및 기본 사용법' 부터 26강 'cookie 2' 까지.

MySQL 소개 및 기본 사용법


이전 강의에서 진행한 OrientDB 버전의 글쓰기 웹 애플리케이션을 MySQL 버전으로 다시 작성하는 강의다.

MySQL이 설치되어 있지 않다면, wamp bitnami를 이용해 MySQL을 설치한다. PHP 관련 프레임워크는 설치할 필요가 없으니 최소화된 set으로 설치하도록 한다. 윈도우 기준 기본 설치 경로는 'C:\Bitnami\wampstack-7.1.16-0' 이고, bitnami에서 제공하는 manager-windows.exe를 통해 Apache 웹 서버와 MySQL의 서비스 동작 상태를 확인할 수 있다.

간단한 명령들..
접속 > mysql -uroot -p {password}
DB목록확인> show databases;
DB생성> CREATE DATABASE o2 CHARACTER SET utf8 COLLATE utf8_general_ci;
DB사용> use o2;
테이블 생성>
CREATE TABLE `topic` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `title` varchar(100) NOT NULL,
 `description` text NOT NULL,
 `author` varchar(30) NOT NULL,
 PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
show tables;

//

Node.js - MySQL 연동.


npm install node-mysql --save
사용법: https://www.npmjs.com/package/mysql

1) 이전 강의에서 진행했던 OrientDB가 RDB에서 사용하는 기본적인 형태의 SQL을 지원했기 때문에 module을 불러오고, connection을 얻는 방법, callback으로 꽂히는 데이터의 형태만 확인해보자. (SELECT에선 복수의 RowDataPacket이, INSERT/UPDATE/DELETE에선 단일 OkPacket 객체를 반환한다.)

2) 치환자는 '?'를 쓴다.
var sql = 'INSERT INTO topic (title, description, author) VALUES (?, ?, ?)';
var params = ['Supervisor', 'Watcher', 'graphittie'];
conn.query(sql, params, function (err, rows, fields) { ... });


http


http = Web browser의 요청(request), 그리고 Web server 의 응답(response)에 대한 규약. 이 규약의 핵심은 request headers와 response headers인데 Chrome 검사 도구의 Network 탭에서 상세한 내용을 확인할 수 있다.


cookie 1


쿠키는 브라우저 환경에서 사용자의 상태를 유지하기 위한 작은 기록 정보 파일을 일컫는다. 이 수단은 넷스케이프의 개발자 루 몬틀리가 고안한 뒤로 지금까지도 많이 쓰이고 있다. 쿠키는 컴퓨터내에서 프로그램처럼 실행될 수 없으며 바이러스를 옮길 수도, 악성코드를 설치할 수도 없다. 하지만 스파이웨어를 통해 유저의 브라우징 행동을 추적하는데에 사용될 수 있고, 누군가의 쿠키를 훔쳐서 해당 사용자의 웹 계정 접근권한을 획득할 수도 있다. (from wikipedia)

cookie를 테스트해보는 예제로 count 앱을 구현하기로 한다. cookie 기능을 사용하기 위해선 cookie-parser라는 미들웨어를 이용해야 한다.

설치>
npm install cookie-parser --save

설치하고 나면 코드에서는,
var cookieParser = require('cookie-parser');
app.use(cookieParser());

와 같이 해준다.
count 앱은 브라우저에 쿠키에 지정된 count 값을 출력해주는 단순한 앱이다. response의 cookie에 값을 지정하면, request의 cookie에 서버가 지정해준 값이 유지되는 것을 확인할 수 있다.

app.get('/count', function (req, res) {
  if (req.cookies.count) {
    var count = parseInt(req.cookies.count);
  } else {
    var count = 0;
  }
  count = count + 1;

  res.cookie('count', count);
  res.send('count : ' + count);

});


cookie2


두번째 예제로 쿠키를 이용해 쇼핑몰 카트와 유사한 기능에 대한 것이다. 쿠키를 이용해 사용자의 입력을 쿠키를 이용해 유지할 수 있는 구조와 동작을 구현해본다.



그리고 나서 cart 목록을 출력하는 구문을 작성한다.
uri는 '/cart', method는 'get'.


마지막으로 쿠키와 관련된 구현에서 개발자는 보안에 신경을 써주어야 한다. 쿠키 값에 대해 보안을 고려하지 않으면 자칫 민감하게 다루어져야 할 정보가 그대로 유출될 수 있기 때문이다. 특히 사용자의 아이디와 암호와 관련된 정보는 절대로 쿠키에 기록하지 않도록 하자. https나 쿠키 값에 대한 암호화를 사용한다 하더라도 이는 올바른 구현 방법이 아니다.

쿠키에 대한 암호화 방법은 다음과 같다. 우선 cookieParser 미들웨어에 대한 사용을 선언할 때 암호화-key를 지정해줄 수 있다. 그렇게 하고나서 아래와 같이 req에선 signedCookies를, res에선 { signed: true } 옵션을 지정하는 것을 통해 암호화된 쿠키 정보를 다룰 수 있다.

app.use(cookieParser('your-secret-key'));

app.get('/count', function (req, res) {
  if (req.signedCookies.count) {
    var count = parseInt(req.signedCookies.count);
  } else {
    var count = 0;
  }
  count = count + 1;

  res.cookie('count', count, { signed: true });
  res.send('count : ' + count);
});

2018년 4월 4일 수요일

Node.js - 스터디 2주차 - OrientDB

https://opentutorials.org/course/2136/

생활코딩 강의를 이용해 진행하는 스터디 2주차.

4/5(목) :: 20강 'OrientDB 소개 및 기본 사용법' 부터 21강 'OrientDB로 웹애플리케이션 구현' 까지.

강의에서 사용하는 OrientDB는 처음 알게된 DB인데 내용이 꽤나 흥미롭다. MongoDB를 사용해본 적이 없어서 MongoDB로 수업이 진행 되었으면 더 좋았겠지만..

OrientDB

OrientDB는 NoSQL(Not Only SQL) 이면서 GraphDB이다. NoSQL 솔루션은 많이 알려졌듯이 RDB에 비해 확장성, 스키마 유연성 측면에서 강점이 있다.

GraphDB?

그래프 데이터베이스는 그래프 이론에 토대를 둔 일종의 NoSQL 데이터베이스이다. 객체나 노드로 불리는 데이터 포인트를 플롯하고, 이들의 관계를 선으로 연결해 데이터의 복잡한 관계를 더 쉽고 빠르게 파악하게 해준다.

1) 관계형 데이터베이스는 데이터를 열과 행으로 저장한다.
2) NoSQL 데이터베이스는 많은 비정형 데이터를 저장한다.
3) 그래프 데이터베이스는 더 나아가 데이터 포인트를 연결, 데이터 네트워크를 구축한다.

무엇이 더 좋냐 나쁘냐의 문제는 아니고, 해결하려는 문제에 더 적합한 데이터베이스의 형태가 있다고 할 수 있겠다. 어쨌든 OrientDB에서 제공하는 그래프 화면은 아래와 같다.


OrientDB는 MongoDB와 같이 도큐먼트 베이스로 동작하지만 Object-Oriented 특성을 추가로 갖는 특징이 있고, RDB에서 사용하던 일반적인 SQL 구문을 이용해 데이터를 쉽게 찾을 수 있는 등의 이점이 있다.

OrientDB의 설치와 초기 설정

Enterprise는 돈을 지불해야 사용할 수 있는 것 같고, Community Edition을 내려받으면 된다. (*OrientDB는 Java로 만들어진 데이터베이스 엔진이기 때문에 사전에 Java가 설치되어 있어야 한다.) 설치방법은 매우 간단하다. Windows에서도 그냥 압축파일 형태로 되어있고, 아무데서나 압축을 해제한 후 생성된 bin 폴더의 server.bat를 시작하면 데이터베이스 서비스가 시작된다. (server.bat 파일을 열어보면 힙 메모리 크기, 동작 머신-x64/x86 등을 지정할 수 있으니 필요하면 건드리자.)

server.bat를 처음 시작하면 password를 물어보는데 root password에 해당되니 기억해두자. 서비스가 시작되고 나서 웹 브라우저를 이용해 localhost의 2480 포트로 접속하면 데이터베이스 관리자 화면을 확인할 수 있다. 여기서 데이터베이스를 생성하거나 스키마를 지정해줄 수 있다. 참고로 데이터베이스의 기본 접속 포트는 2424이고, 데이터베이스 GUI 관리자 화면의 접속 포트는 2480이 기본값이다.



Node.js에서 OrientDB 사용하기.


Node.js에서 OrientDB를 사용하려면 orientjs라 하는 모듈이 설치되어 있어야 한다.
npm install orientjs --save

사용 흐름과 쿼리 방식은 아래 스크린 샷과 같은데. 크게 어렵거나 하지 않아서. 사진으로 대체한다.


insert 후엔 insert 결과를 반환하는데 여기에서 @rid를 얻을 수 있다.
update/delete후엔 영향을 받은 row 수가 ['1'] 과 같은 형태로 반환된다.

OrientDB로 웹애플리케이션 구현

파일 기반으로 작성된 웹애플리케이션을 OrientDB로 변환하는 과정에 대한 내용이다. 아래와 같이 강사님처럼 API 지도를 만들어주면 전체적인 흐름을 파악하기에 좋다.

get('topic/') : view.jade
get('topic/:id') : view.jade
get('topic/add') : add.jade
ㄴpost('topic/add')
ㄴget('topic/:id')
get('topic/:id/edit') : edit.jade
ㄴpost('topic/:id/edit')
ㄴget('topic/:id')
get('topic/:id/delete') : delete.jade
ㄴpost('topic/:id/delete')
ㄴget('topic/')

자료를 OrientDB로 부터 읽어와 화면에 전달하는 로직은 크게 어렵지 않지만 주의할 점이 몇가지 있다.

1) OrientDB의 식별자는 '@rid'란 이름을 갖는데, jade에서 object.@rid와 같이 표현할 경우 에러를 뱉는다. 따라서 object['@rid']와 같은 표현으로 식별값에 접근해야 한다.

2) @rid의 값은 '#숫자:숫자' 형식으로 되어있는데, @rid를 이용해 링크 URL 등을 생성할 경우 '#', ':' 문자에 대한 처리를 해주어야 한다. '#'의 경우 페이지의 특정 위치로 스크롤하는데 쓰이는 특수 기호이기 때문이다. 강의에서는 encodeURIComponent(topic['@rid']) 와 같은 처리를 통해 @rid 값을 인코딩해 서버로 전달되게끔 구현하였다.

view.jade의 풀 소스는 다음과 같다. if / else 구문을 jade에 바로 사용할 수 있는 부분이 반갑다. '|'는 태그 없이 문자열을 출력하고 싶을 때 사용하면 된다.

doctype html
html
  head
    meta(charset='utf-8')
  body
    h1
      a(href='/topic/') Server Side JavaScript
    ul
      each topic in topics
        li
          - rid = encodeURIComponent(topic['@rid'])
          a(href='/topic/' + rid)= topic.title
    article
      if topic
        h2= topic.title
        = topic.description
      else
        h2 Welcome
        | This is server side javascript tutorial.
      br
    div
      a(href='/topic/new') new

이에 대한 핸들링 함수는 아래와 같다. 지저분해 보이지만 일단 동작하는게 우선.


조회 부분을 구현하고나면 다음. 추가 / 편집 / 삭제은 비교적 편하게 구현할 수 있다.
개발 과정에서 잘 모르는 건 console이든 browser로 보내든 output을 찍고 보는 습관.

추가 - INSERT

app.post('/topic/add', function(req, res) {
  var title = req.body.title;
  var description = req.body.description;
  var author = req.body.author;
  var sql = 'INSERT INTO topic (title, description, author) VALUES (:title, :description, :author)';
  db.query(sql, {
    params: {
      title: title,
      description: description,
      author: author
    }
  }).then(function (results) {
    res.redirect('/topic/' + encodeURIComponent(results[0]['@rid']));
  });
});

편집 - UPDATE

편집 기능을 선택하면. 조회 로직과 유사하게 선택된 아이템의 값을 가져와 화면에 뿌려주는 부분을 구현하면 된다. 그리고나서 action의 URI를 edit로 지정하고, edit POST 요청에 대한 서비스를 구현하면 된다.

jade
    article
      - rid = encodeURIComponent(topic['@rid'])
      form(action='/topic/' + rid + '/edit' method='post')
        p
          input(type='text' name='title' placeholder='title' value=topic.title)
        p
          textarea(name='description' placeholder='description')
            =topic.description
        p
          input(type='text' name='author' placeholder='author' value=topic.author)
        p
          input(type='submit')

js
app.post('/topic/:id/edit', function (req, res) {
  var sql = 'UPDATE topic SET title=:t, description=:d, author=:a WHERE @rid=:id';
  var id = req.params.id;
  var title = req.body.title;
  var desc = req.body.description;
  var author = req.body.author;
  db.query(sql, {
    params: {
      id: id,
      t: title,
      d: desc,
      a: author
    }
  }).then(function(topics) {
    res.redirect('/topic/' + encodeURIComponent(id));
  });
});

삭제 - DELETE

특별한 내용은 없지만 주의할 점이 있다. 데이터에 변경을 가하는 INSERT / UPDATE / DELETE 등의 작업에 대해 GET 메서드를 사용하지 않는 것과 더불어 그러한 기능을 수행하는 태그에 <a> 태그를 쓰지 않도록 한다. <a> 태그를 사용하는 경우에 크롤링 엔진 등에 의해 해당 기능이 자동 실행될 수 있어 예기치 않게 데이터에 손상을 입힐 수 있기 때문이다.

jade
    article
      h1= 'Delete? ' + topic.title
      - rid = encodeURIComponent(topic['@rid'])
      form(action='/topic/' + rid + '/delete' method='post')
        p
          input(type='submit' value='YES')
      a(href='/topic/' + rid) No

js
app.post('/topic/:id/delete', function (req, res) {
  var sql = 'DELETE FROM topic WHERE @rid=:id';
  var id = req.params.id;
  db.query(sql, { params: { id: id }}).then(function(topics) {
    res.redirect('/topic/');
  });
});