2018년 5월 1일 화요일

라즈베리파이 다루기

라즈베리파이(raspberrypi.org)는 2012년 1월 영국의 라즈베리 파이 재단에서 교육 목적으로 제작 발표한 싱글 보드 컴퓨터다. 지금까지도 가장 성공한 오픈소스 하드웨어 중 하나로 꼽히며 적은 비용으로 꽤 괜찮은 개인 사용 목적의 머신을 구성할 수 있다는 장점이 있다.

한창 인기있었을 때의 RPi2 버전에서 최근엔 RPi3 B+ 까지 출시된 상태다. 하드웨어는 계속 업그레이드 되고 있음에도 가격은 35$를 유지하는 점이 매력적이다. RPi3 부터는 64비트를 지원하며 블루투스, 무선랜이 내장되어 있어 키보드와 마우스, 그리고 라즈비안이 설치된 micro SD 카드만 있으면 일반적인 PC처럼 바로 사용할 수 있다.

라즈베리파이와 자주 비교되는 하드웨어는 비글보드, 인텔 갈릴레오, 엔비디아 젯슨 등이 있지만 개인 DIY 목적에선 라즈베리파이가 가장 나은 선택이 될것으로 보인다. HW 특성이나 비교 관련해서 자세한 내용은 나무위키를 참조할 것. 커뮤니티는 oroca.org가 괜찮아 보인다.
https://namu.wiki/w/%EB%9D%BC%EC%A6%88%EB%B2%A0%EB%A6%AC%20%ED%8C%8C%EC%9D%B4(%EC%BB%B4%ED%93%A8%ED%84%B0)

라즈베리 파이 동작에 필요한 것들.


1. 키보드와 마우스
2. 라즈베리파이를 지원하는 운영체제가 담긴 micro SD 카드
3. 전원 (스마트폰 충전기)
4. HDMI 케이블


라즈베리 파이 주요 지원 운영체제


1. 라즈비안 - 추천. 데비안 리눅스 기반으로 LXDE 데스크탑 환경, 개발 도구까지 포함되어 있어 편리하다. (*LXDE: Lightweight X11 Desktop Environment, 경량 X 윈도우 GUI 환경)
2. 우분투 리눅스
3. 윈도 10 IoT
4. OSMC, OpenELEC
5. RISC OS


라즈비안 설치


1. raspberrypi.org/downloads에서 배포판 이미지를 다운로드 한다.
2. Win32DiskImager를 이용해 micro SD 카드에 설치한다.
3. micro SD 카드를 라즈베리파이 보드에 삽입하고, 전원을 연결한다.
- 초기 접속 계정은 pi/raspberry 이다.


초기 환경설정


1. 터미널에서 sudo raspi-config를 실행하거나, GUI에서 preferences > rasiberry pi configuration 경로를 통해 설정 화면으로 진입할 수 있다.
2. 언어 설정(Change Locale) ko_KR_EUC-KR, ko_KR.UTF-8을 추가한다.
3. 시간 설정(Change Timezone) Asia/Seoul을 지정한다.
4. 키보드 설정(Change Keyboard Layout) 최근 버전에 Korean 배치가 있으니 Korean을 지정한다. 없으면 Generic 105-key로.
5. Interfacing Options에서 SSH와 VNC를 활성화 시킨다. VNC는 GUI 리모트 접속에 대한 설정을 의미한다. VNCViewer 클라이언트를 이용하면 윈도우에서 라즈베리파이로 원격 접속이 가능하다.


한글 설정


1. sudo apt-get update # 최신 정보와 sync를 맞추기 위함.
2. sudo apt-get upgrade # 최신 정보로 실제 업그레이드 수행.
3. sudo apt-get install ibus ibus-hangul # 한글 입력기 설치
4. sudo apt-get install fonts-unfonts-core # 폰트 설치


네트워크 설정


RPi3에선 무선랜을 지원하므로 그냥 우상단의 네트워크 아이콘을 클릭해서 무선 네트워크에 접속하면 된다.
* ifconfig - 유선 설정 / iwconfig - 무선 설정
설정 파일은 /etc/network/interfaces에 있다.


라즈베리파이 GPIO


1. GPIO: 일반적인 용도로 사용 가능한 디지털 입출력 핀. 하드웨어 제어를 위한 신호를 생성하거나 하드웨어에서 들어오는 신호를 받아들이기 위해 사용한다. 터미널에서 gpio readall 명령어를 입력하면 라즈베리파이의 GPIO 핀맵을 확인할 수 있다.
2. 주의점: 라즈베리파이는 내부 3.3V 전원으로 동작되므로 GPIO 핀들도 3.3V 정도로 동작한다. (5V 신호를 사용하고 싶으면 레벨 컨버터가 필요하다.) GPIO 출력인 경우 가능하면 핀 당 3mA 이하, 총합 50mA 이하로 전류 공급을 제한하는 것이 안전하다.
3. GPIO 제어: 간단하게는 파일을 통해 접근하는 방식인 sysfs를 이용할 수 있다. 프로그래밍(응용) 레벨에선 RPi.GPIO 또는 WiringPI 모듈이 주로 쓰인다. 복잡도, 편의 측면에서 파이썬이 나은 선택으로 보인다. 아래와 같이 간단하고 직관적인 인터페이스를 제공하고 있다.

import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN_NO, GPIO.OUT)
GPIO.output(PIN_NO, 0)
GPIO.output(PIN_NO, 1)
GPIO.cleanup(PIN_NO)

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