파워쉘을 가장 처음 접했을 때 확장자에 숫자가 있어서 어떤 의미인지 궁금했는데 오늘 잠시 검색해보고 내용을 정리했다. 먼저 결론을 얘기하면 버전과 상관 없이 .ps1이 파워쉘 스크립트의 확장자다.

파워쉘은 이전까지 Monad Manifesto라는 Windows Command Shell 프로젝트였는데 파워쉘이라는 이름으로 릴리즈 되면서 이전까지 쉘에서 사용하던 .msh 확장자를 .ps1으로 변경했다. .ps는 포스트스크립트(PostScript)의 확장자로 오랜 기간 동안 사용해왔다. 그래서 .ps를 선택할 수 있는 옵션은 아니였던 것으로 보인다. 그리고 버전을 표기하기 위해서 .ps1 이라는 확장자를 선택하게 되었다.

하지만 PowerShell의 2 버전을 내면서 1) 1 버전의 스크립트가 2 버전에서 호환된다, 2) 2 버전이 1 버전을 대체한다는 이유로 .ps1 확장자를 그대로 사용하기로 했다. 대신 각 스크립트에서 요구하는 버전을 선언할 수 있도록 #REQUIRES 문을 제공하게 되었다. 그 이후로는 그냥 .ps1을 아무 고민 없이 사용하게 되었다는 훈훈한 이야기다.

#REQUIRES문을 다음과 같이 추가해서 사용할 버전을 지정할 수 있다.

#REQUIRES -version 5

현재 구동하고 있는 파워쉘보다 높은 버전이라면 다음처럼 오류가 발생한다. (6 버전은 아직 존재하지 않는다. 그냥 큰 숫자를 넣어서 구동한 결과다.)

.\req.ps1 : The script 'req.ps1' cannot be run because it contained a "#requires" statement for Windows PowerShell 6.0. The version of Windows PowerShell that is required by the script 
does not match the currently running version of Windows PowerShell 5.0.10586.122.
At line:1 char:1
+ .\req.ps1
+ ~~~~~~~~~~
    + CategoryInfo          : ResourceUnavailable: (req.ps1:String) [], ScriptRequiresException
    + FullyQualifiedErrorId : ScriptRequiresUnmatchedPSVersion

현재 구동하고 있는 파워쉘의 버전은 $PSVersionTable에서 확인할 수 있다.

PS> $PSVersionTable.PSVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
5      0      10586  122

Get-Host로 확인할 수 있는 버전은 파워쉘에 접속하는 호스트의 버전을 의미하지 쉘의 버전을 뜻하지 않기 떄문에 주의해야 한다.

만약 상위 호환에 문제가 있는 파워쉘 스크립트를 구동하려면 어떻게 해야 할까? 그런 경우는 PowerShell의 플래그 -Version을 이용해서 구동할 수 있다.

PowerShell -Version 2 .\hello.ps1

실제로 구동 결과를 보면 하위 호환이 지원 되는 가장 마지막 쉘로 구동하는 방식이다. $PSVersionTable을 출력하도록 확인해보면 1, 2 버전은 2 버전으로, 3 버전 이상은 그냥 현재 버전으로 구동하게 된다.

psh 같은 확장자를 선택했으면 좀 멋지지 않았을까 생각이 든다.

더 읽을 거리

얼마 전에 Windows 환경이 필요해 lubuntu 설치해서 사용하던 노트북을 Windows 10으로 전환했다. 이 노트북은 32GB eMMC 내장이라 사실 공간이 엄청 부족한 편이다. Windows 10을 설치하고 나니 5GB만 남아서 Visual Studio는 설치할 엄두조차 내지 못했다. 때마침 Microsoft PowerShell이 정말 좋다는 이야기를 계속 들었던 것이 생각나서 잠깐 살펴보게 되었다. 이런 강력한 쉘이 Windows에 기본 내장인걸 이제야 알았다는 게 분할 정도로 많은 기능이 기본으로 제공되고 있었다. 그래서 몇 가지 간단한 도구를 공부 삼아 만들어봤고 정말 만족스러웠다.

PowerShell Website

Microsoft PowerShell은 Windows XP 이후로 꾸준히 탑재된 명령행 쉘로 .Net Framework으로 개발된 스크립트 언어가 내장되어 있다. 이전에도 JScript나 VBScript가 있었지만 쉘과 연동하기 어려운 문제와 보안 등의 이유로 인해 문제가 계속 제기되었고 그 대안으로 개발된 것이 이 PowerShell이다. .Net Framework과 연동해 다양한 기능을 제공하는 cmdlet과 스크립트, 기본 함수 등은 정말 이 파워쉘 하나만 갖고도 수많은 작업을 쉽게 처리할 수 있을 정도로 탄탄하며 세세하면서도 넓은 영역의 기능을 제공하고 있다. PowerShell Gallery라는 별도의 모듈 관리자도 존재한다. 심지어 dll을 불러서 호출하는 것도 가능하기 때문에 스크립트 언어가 제공하는 기능에 제한받지 않는다는 점이 인상적이다. 너무 자랑만 한 것 같아서 단점은 Windows에서만 제대로 돌아간다는 정도1가 아닐까 싶다. 😛

이 포스트에서는 Microsoft PowerShell을 사용해서 간단한 스크립트를 작성해보고 어떤 방식으로 동작하는지, 얼마나 간편한지 확인(및 영업)을 하려고 한다. 다음은 이 포스트에서 살펴보게 될 내용이다.

  • 텔레그램 봇을 생성하고 토큰 발급하기
  • 텔레그램 봇의 정보를 API로 확인하기 (getMe, getUpdates)
  • 봇 API의 token과 사용자의 chat_id를 설정 파일로 분리하기
  • 웹페이지 호출한 다음 결과물 가공하기
  • 텔레그램 봇 API로 메시지 전송하기
  • 새 메시지만 전송하도록 메시지 비교하고 csv로 저장하기

이 글에서 필요한 준비물은 다음과 같다.

  • PowerShell 구동 가능한 Windows 환경
  • 메신저 서비스인 텔레그램(telegram) 계정

전체 코드는 haruair/ps-telegram-message에서 확인할 수 있다.

텔레그램 봇 생성하기

텔레그램 봇을 생성하려면 @BotFather 계정에 대화를 신청해서 쉽게 생성할 수 있다. telegram.me/BotFather 링크를 누르거나 @BotFather를 직접 검색해서 추가한다. 봇을 생성하면 이 봇을 사용할 때 넣어야 하는 API 토큰을 발급해주는데 모바일에서는 컴퓨터로 복사하기 불편할 수 있다. 텔레그램 웹에서 생성하면 편리하다.

@BotFather에게 /newBot을 입력하면 새로운 봇 이름과 봇 계정명을 순서대로 입력하라고 안내한다. 순서대로 입력하고 나면 API 토큰과 해당 봇 링크를 알려준다.

botfather

이 토큰을 이용해서 API를 사용하는 방법은 Telegram Bots에서 자세히 확인할 수 있다. (참고로, 위 이미지의 API 토큰은 더이상 동작하지 않는다.)

텔레그램 봇 정보 확인하기

앞에서 생성한 토큰을 사용해서 텔레그램 봇에 정상적으로 접근할 수 있는지 확인하려 한다. 파워쉘은 ps1이라는 확장자를 사용한다. status.ps1라는 파일을 다음 내용으로 작성한 후에 저장한다.

$response = Invoke-WebRequest -Uri "https://api.telegram.org/bot<API 토큰>/getMe"
echo $response.RawContent
pause

텔레그램 봇 API의 getMe 메서드 주소로 요청을 보내는 코드를 작성했다.

이 코드에서 Invoke-WebRequest 라는 함수를 확인할 수 있다. 이런 함수는 PowerShell 환경에서만 사용할 수 있도록 구현된 함수로 Cmdlet으로 부른다. (Command-let으로 읽는다.) Invoke-WebRequest cmdlet은 HTTP, HTTPS, FTP 또는 FILE 프로토콜로 웹페이지나 웹서비스에 요청을 보낼 수 있는 기능을 제공한다. 이 함수가 반환한 HtmlWebResponseObject 개체를 $response에 저장했다.

Invoke-WebRequest에서 오류가 발생하면 포스트 마지막에 있는 문제 해결을 참고한다.

2행에서 echo를 사용해서 $responseRawContent 프로퍼티를 출력한다. echoWrite-Output cmdlet의 축약 표현이다.

파워쉘을 실행하면 결과를 처리하고 바로 창이 닫힌다. 3행의 pause로 엔터 키를 입력하기 전까지 창이 닫히지 않게 된다. 이후의 코드에서는 별도로 표기하지 않으니 언급이 없으면 코드 마지막 행에는 pause가 있다고 생각하자.

파일을 더블 클릭으로 열면 설정에 따라 메모장으로 열릴 수도 있다. 파일에서 오른쪽 클릭 후 **PowerShell에서 실행 (Run with PowerShell)**을 클릭한다. 이 저장한 파일을 실행하면 다음처럼 출력되는 것을 확인할 수 있다.

파워쉘 명령행에서 실행했을 때 오류가 발생하면 이 포스트의 문제 해결 부분을 참고한다.

HTTP/1.1 200 OK
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Expose-Headers: Content-Length,Content-Type,Date,Server,Connection
Strict-Transport-Security: max-age=31536000; includeSubdomains
Content-Length: 98
Content-Type: application/json
Date: Fri, 08 Jul 2016 09:50:00 GMT
Server: nginx/1.10.0

{"ok":true,"result":{"id":2775591989,"first_name":"webscrapbot","username":"haruair_webscrap_bot"}}

Press Enter to continue...:

응답 헤더와 json 형식의 응답 내용을 확인할 수 있다. pause로 인해 창이 닫히지 않고 엔터를 입력하면 그제서야 닫힌다.

$info = Invoke-RestMethod -Uri "https://api.telegram.org/bot<API 토큰>/getMe"
echo $info.ok

이 코드에서는 앞과 다르게 Invoke-RestMethod 라는 함수를 확인할 수 있다. Invoke-RestMethod cmdlet은 REST 웹서비스를 호출하고 그 반환된 결과를 구조화된 데이터로 사용할 수 있는 기능을 제공한다. 별도의 라이브러리 없이 간편하게 REST 호출이 가능할 정도로 기본적으로 제공하는 기능이 다양하다.

앞에서 확인한 코드에서는 HtmlWebResponseObject를 반환해서 RawContent 프로퍼티로 접근할 수 있었던 반면에 Invoke-RestMethodPSCustomObject를 반환한다. 그래서 JSON 개체를 쉽게 프로퍼티처럼 접근해서 사용할 수 있기 때문에 편리하다. 2행의 $info.ok는 앞에서 확인했던 응답 내용과 같이 true를 반환한다.

앞 응답 내용에서 first_name, username처럼 result 내에 있는 데이터는 어떻게 확인할 수 있을까? 다음과 같은 코드를 추가하면 쉽게 확인할 수 있다.

$info = Invoke-RestMethod -Uri "https://api.telegram.org/bot<API 토큰>/getMe"

if ($info.ok) {
    echo $info.result | Format-List
}

$info.ok가 참이면 $info.result의 내용을 출력한다. 여기서 | 기호를 사용해서 Format-List를 추가했다. 코드를 실행하면 다음과 같은 결과를 확인할 수 있다.

id         : 2775591989
first_name : webscrapbot
username   : haruair_webscrap_bot

| 기호 즉, 파이프라인(pipeline)은 다른 쉘에서도 쉽게 볼 수 있는 기능으로 파워쉘도 동일하게 지원한다. 이 파이프라인을 사용하면 순서대로 앞 결과를 뒤 명령에서 입력으로 사용한다. 위 코드에서는 $info.resultFormat-List의 입력으로 사용하게 된다. 파워쉘에서는 결과를 보기 쉽게 목록으로 변환하는 Format-List를 제공한다. 이 cmdlet도 fl로 줄여 쓸 수 있다. 비슷하게 Format-Table은 표로 변환하며 특정 양식으로 변환할 수 있는 Format-Custom도 있다.

지금까지 텔레그램에 등록한 봇을 API로 접근할 수 있다는 점을 확인했다. 이제 텔레그램에 메시지를 전송하기 전에 사용자의 chatId를 알아내야 한다. 이 chatId를 알아내려면 어떻게 해야 할까? 봇이 받은 메시지를 확인하면 그 메시지를 보낸 사용자의 chatId를 찾을 수 있다. 받은 메시지는 getUpdates 메서드를 사용해서 확인 가능하다. 다음 코드를 추가해보자.

$updates = Invoke-RestMethod -Uri "https://api.telegram.org/bot<API 토큰>/getUpdates"

if ($updates.ok) {
    $updates.result | Select-Object -expandProperty message | Select-Object -expandProperty from text | Format-Table
}

앞에서 본 내용과 거의 비슷하다. Select-Object cmdlet은 개체 또는 개체의 프로퍼티에서 필요한 부분만 선택하는 기능을 제공한다. 이 cmdlet과 함께 사용한 -expandProperty 매개 변수는 특정 프로퍼티의 내용을 확장해서 데이터를 처리할 수 있게 한다. 각 파이프 별로 잘라서 실행해보면 그 변화를 확인할 수 있다. 이처럼 파이프라인은 여러 결과를 이어서 사용할 수 있으며 마지막 cmdlet으로 Format-Table을 사용해 표 형식으로 출력했다.

앞과 다르게 echo를 입력하지 않았다. echo 없이 작성해도 넣고 작성한 것과 동일한 결과가 나온다.

이제 이 코드를 실행해보면 빈 표가 출력되거나 에러가 발생한다. 그 이유는 봇과 텔레그램을 통해 메시지를 전송한 적이 없기 때문이다. 스팸 문제 때문인지 이 chatId는 실제로 해당 봇과 대화를 한 적이 있는 경우에만 얻을 수 있다. 텔레그램 봇 생성하기에서 받은 봇 링크(http://telegram.me/<생성한 Bot ID>)를 클릭한 다음에 /start 또는 아무 내용이나 메시지를 작성해서 전송한다. 전송한 다음에 스크립트를 실행하면 다음과 같은 결과를 확인할 수 있을 것이다.

       id first_name last_name username  text  
       -- ---------- --------- --------  ----  
138389563 Edward     Kim       haruair   /start
138389563 Edward     Kim       haruair   Hello

결과에서 확인할 수 있는 것처럼 내 chatId는 138389563이다. 이 chatId를 사용하면 봇이 나에게 메시지를 전송하게끔 할 수 있다.

지금까지 작성한 status.ps1을 정리하면 다음과 같다.

$info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/getMe"

if ($info.ok) {
    echo $info.result | Format-List
}

$updates = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/getUpdates"

if ($updates.ok) {
    $updates.result | Select-Object -expandProperty message | Select-Object -expandProperty from text | Format-Table
}
pause

설정 파일 분리하기

Bot API를 호출할 때 사용하는 토큰과 chatId는 별도의 설정 파일로 분리하면 깔끔하게 사용할 수 있다. config.json 파일을 생성하고 다음처럼 내용을 작성한다. 각 항목은 내용에 맞게 입력한다.

{
    "token": "<API 토큰>",
    "chatId": "<메시지를 받을 chat_id>" 
}

이 json 파일을 사용하도록 status.ps1을 다음 코드를 추가한다.

$config = Get-Content .\config.json | ConvertFrom-Json

Get-Content cmdlet은 파일 내용을 불러온 후에 ConvertFrom-Json cmdlet에게 그 내용을 전달한다. ConvertFrom-Json는 JSON을 개체로 변환해서 $config에 저장한다.

이제 토큰은 $config.token 식으로 접근해서 사용할 수 있다. 이제 코드를 수정해보자. 파워쉘의 문자열 내에서는 $(변수) 형식으로 문자열 보간(String Interpolation)이 가능하다. 여기까지 진행한 status.ps1은 다음과 같다.

$config = Get-Content .\config.json | ConvertFrom-Json

$info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/getMe"

if ($info.ok) {
    echo $info.result | Format-List
}

$updates = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/getUpdates"

if ($updates.ok) {
    $updates.result | Select-Object -expandProperty message | Select-Object -expandProperty from text | Format-Table
}

pause

Get-Content를 사용할 때 주의해야 하는 점은 기본 인코딩이 따로 지정되어 있지 않다는 부분이다. 인코딩을 지정하지 않아도 큰 문제가 되지 않는 범위의 내용을 저장한다면 문제가 없지만 한글은 제대로 불러오지 못한다. 그래서 -encoding 매개 변수를 이용해서 utf8로 불러오면 문제 없이 불러올 수 있게 된다. 다음 코드를 참고하자.

$config = Get-Content .\config.json -Encoding utf8 | ConvertFrom-Json

웹페이지 호출한 다음 결과물 가공하기

이제 본격적으로 메시지를 통해 보낼 데이터를 가공하려고 한다. message.ps1을 생성해서 다음 내용을 작성한다.

$haruair = Invoke-WebRequest -Uri "http://haruair.com/blog/"
$titles = $haruair.ParsedHtml.getElementsByTagName("h2") | Where-Object { $_.className -eq "entry-title" }
$links = $titles.getElementsByTagName("a") | Select-Object innerText, href

1행에서는 http://haruair.com/blog/ 페이지의 내용을 Invoke-WebRequest cmdlet으로 가져왔다. 이 페이지에는 여러 포스트가 한번에 출력된 목록 페이지로 각각의 제목과 링크를 가져와서 메시지를 보내는데 활용하려고 한다.

2행은 ParsedHtml 프로퍼티에 접근한 다음에 getElementsByTagName 메서드를 사용해서 포스트 제목에 해당하는 엘리먼트인 <h2>를 선택했다. 그 다음에 파이프라인을 이용해서 Where-Object cmdlet으로 넘겼고 html 엘리먼트의 클래스명을 기준으로 필요한 제목만 선택했다. Where-Object는 개체 컬렉션에서 특정 프로퍼티의 값을 사용해 개체를 선택할 수 있는 cmdlet이다. Where 또는 ?로 표기할 수 있다.

이제 제목 엘리먼트에서 제목과 해당 포스트로 이동할 링크를 찾을 차례다. 3행을 보면 선택한 <h2> 엘리먼트에서 <a>를 다시 선택한 후에 앞에서 봤던 Select-Object를 활용해서 엘리먼트 내용과 링크만 선택한 다음 $links에 반환했다.

텔레그램 봇 API로 메시지 전송하기

이제 텔레그램으로 메시지를 전송하려고 한다. message.ps1에 다음 내용을 추가한다.

foreach ($link in $links) {
    $message = "New Post! $($link.innerText) $($link.href)"
    $encodedMessage = [System.Web.HttpUtility]::UrlEncode($message)
    $info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/sendMessage?text=$encodedMessage&chat_id=$($config.chatId)"
}

$linksforeach 문으로 반복했다. $message에 전송할 메시지를 작성했다.

3행에서는 지금까지 보지 못했던 특이한 문법이 존재한다. System.Web.HttpUtility는 .Net Framework에 포함된 클래스로 URL을 인코딩 또는 디코딩 하는 메서드를 제공한다. 파워쉘에서는 .Net의 클래스와 메서드를 직접적으로 사용할 수 있다. 여기서는 [System.Web.HttpUtility]::UrlEncode를 사용해서 메시지를 인코딩했다.

4행은 API의 sendMessage 메서드를 사용해서 메시지와 메시지를 받을 chat_id를 전달했다.

지금까지 작성한 message.ps1을 종합하면 아래 코드와 같다.

$config = Get-Content .\config.json -Encoding utf8 | ConvertFrom-Json

$haruair = Invoke-WebRequest -Uri "http://haruair.com/blog/"
$titles = $haruair.ParsedHtml.getElementsByTagName("h2") | Where-Object { $_.className -eq "entry-title" }
$links = $titles.getElementsByTagName("a") | Select-Object innerText, href

foreach ($link in $links) {
    $message = "New Post! $($link.innerText) $($link.href)"
    $encodedMessage = [System.Web.HttpUtility]::UrlEncode($message)
    $info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/sendMessage?text=$encodedMessage&chat_id=$($config.chatId)"
}

이 파일을 실행하면 메시지가 잘 전달되는 것을 확인할 수 있다. 하지만 스크립트를 실행 할 때마다 글이 전송된다. 이전에 전송한 글은 전송하지 않게 하려면 어떻게 해야 할까?

메시지 비교하고 csv로 저장하기

파워쉘은 Import-CsvExport-Csv cmdlet으로 손쉽게 CSV를 다룰 수 있다. 또한 개체를 비교하는 Compare-Object cmdlet을 사용해서 항목을 비교할 수 있고 SideIndicator 프로퍼티로 비교 결과를 확인할 수 있다.

Import-Csv cmdlet이 존재하지 않는 파일을 불러오려고 할 때는 오류가 발생한다. 파워쉘도 try catch finally문법을 지원한다. 코드에서는 .Net Framework의 _System.IO.FileNotFoundException_로 catch하는 것을 확인할 수 있다.

아래는 최종적인 message.ps1 코드 내용이다.

$config = Get-Content .\config.json -raw -encoding utf8 | ConvertFrom-Json

$haruair = Invoke-WebRequest -Uri "http://haruair.com/blog/"
$titles = $haruair.ParsedHtml.getElementsByTagName("h2") | Where-Object { $_.className -eq "entry-title" }
$links = $titles.getElementsByTagName("a") | Select-Object innerText, href

try {
  $oldLinks = Import-Csv .\data.csv
}
catch [System.IO.FileNotFoundException] {
  $oldLinks = @()
}

$diff = Compare-Object $oldLinks $links -property innerText, href | Where-Object { $_.SideIndicator -eq '=>' }
$measure = $diff | Measure-Object

if ($measure.Count -ne 0) {
  foreach ($link in $diff) {
    $message = "New Post! $($link.innerText) $($link.href)"
    $encodedMessage = [System.Web.HttpUtility]::UrlEncode($message)
    $info = Invoke-RestMethod -Uri "https://api.telegram.org/bot$($config.token)/sendMessage?text=$encodedMessage&chat_id=$($config.chatId)"
  }

  $links | Export-Csv .\data.csv -Encoding utf8
}

Measure-Object는 개체의 수를 셀 때 사용한다. measure로 줄여서 사용할 수 있다. 위 코드에서는 다소 장황하게 사용했는데 다른 방식으로 작성하는 방법도 존재한다. 아래 코드처럼 ForEach-Object cmdlet도 사용할 수 있다. ForEach-Object의 축약은 %로도 줄여서 사용 가능하다. 가장 간단한 방법은 괄호를 이용한 마지막 방법이다.

$measure = $diff | Measure-Object
if ($measure.Count -ne 0) {
    // do something
}

if (($diff | Measure-Object | ForEach-Object { $_.Count }) -ne 0) {
    // do something
}

if (($diff | measure | % { $_.Count }) -ne 0) {
    // do something
}

if (($diff | measure).Count -ne 0) {
    // do something
}

이제 message.ps1을 실행하면 data.csv에 파일이 생성되고 새 글이 올라오지 않는 경우에는 메시지를 전송하지 않는다. data.csv를 열어보면 개체 목록이 csv로 변환된 내용을 확인할 수 있다. 모든 과정이 끝났다. 필요에 따라서 message.ps1을 Windows 작업 스케줄러를 사용해 반복적으로 호출하면 최신의 업데이트 상황을 수시로 확인해서 메시지로 전송받을 수 있다.

위 스크린샷에서 메시지를 전송 받은 결과를 볼 수 있다.


문제 해결

스크립트를 작성하고 구동하는 과정에서 겪은 문제를 정리했다.

실행 정책 ExecutionPolicy

파워쉘 스크립트를 파워쉘 명령행에서 실행하면 다음과 같은 에러가 출력된다.

.\status.ps1 : File W:\path\to\status.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at 
http://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ .\message.ps1
+ ~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess

서명되지 않은 스크립트 파일은 보안상 기본적으로 실행할 수 없다. 현재 실행 정책은 Get-ExecutionPolicy로 확인할 수 있고 Set-ExcutionPolicy cmdlet으로 조정할 수 있다. 기본 정책은 _Restricted_인데 다음 명령을 사용하면 정책 수준을 낮출 수 있다.

Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser

테스트를 모두 완료한 다음에는 보안을 위해 다시 원래대로 돌려놓도록 하자.

Set-ExecutionPolicy -ExecutionPolicy Restricted -Scope CurrentUser

높은 정책 수준에서도 실행 가능하게 하려면 스크립트에 서명을 하는 방법이 있고 제한적인 용도라면 직접 서명(Self-Signing)을 하는 것도 가능하다.

Invoke-WebRequest 오류

Invoke-WebRequest은 Windows에 내장된 Internet Explorer를 내부적으로 참조한다. 그래서 Internet Explorer를 단 한 번도 실행한 적이 없다면 다음과 같은 메시지가 출력된다.

Invoke-WebRequest : The response content cannot be parsed because the Internet Explorer engine is not available, or Internet Explorer's first-launch configuration is not complete. Specify the UseBasicParsing 
parameter and try again. 
At W:\path\to\status.ps1:4 char:6
+ $response = Invoke-WebRequest -Uri "https://api.telegram.org/bot$($config.token ...
+      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotImplemented: (:) [Invoke-WebRequest], NotSupportedException
    + FullyQualifiedErrorId : WebCmdletIEDomNotSupportedException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

IE를 한 번 켰다 끄면 해결 된다.


간단하게 텔레그램을 통해 메시지를 전송하는 스크립트를 파워쉘에서 작성했다. 여기서 다룬 파워쉘의 기능도 극히 일부에 불과하다. 2006년에 출시해서 무려 9년 넘는 기간 동안 성숙해 온 파워쉘은 타입 지원, 클래스 등 쉘 스크립트 답지 않게 폭넓은 기능을 제공한다. 더 자세히 보고 싶다면 MSDN PowerShell에 있는 글이 많은 도움이 될 것이다. 윈도 환경에서 개발을 한다면 꼭 파워쉘을 살펴보도록 하자. 😀

  • Mono 기반의 구현인 Pash도 있다. 조만간 dotnet core 기반의 구현도 나오지 않을까 기대된다. 
  • 윈도 머신이 잠시 필요해서 lubuntu를 설치한 컴퓨터를 다시 Windows로 복구했다. Lubuntu로 밀기 전에 dd 명령으로 이미지를 백업해뒀는데 과정을 따로 기록해두질 않아서 삽질을 좀 하게 되었다. 그래서 이번에는 안전하게 잊을 수 있도록 기록을 남긴다.

    윈도 이미지 만들면서도 이게 괜찮은 방법인지는 반신반의 했는데 일단 복원까지 하는데 성공했으니 별 문제는 없는 것 같다. 일단 내 노트북은 32GB 밖에 되지 않아서 저장용도 usb stick을 사용했다.

    준비물

    • 부팅용 USB
    • 이미지 저장 및 복원용 USB

    준비 과정

    준비 작업은 맥에서 진행했다. 이미지 저장 및 복원용 USB는 exFat으로 포맷한다. 맥과 리눅스에서 모두 사용할 수 있는 형식이고 용량 문제도 없다. Disk Utility 에서 포맷할 수 있다.

    남은 작업을 편하게 하기 위해 ubuntu live usb를 만든다. 여기서는 설치에 사용했던 lubuntu 이미지를 사용했다.

    $ hdiutil convert -format UDRW -o /path/to/lubuntu-15.10-desktop-amd64 lubuntu-15.10-desktop-amd64.iso
    

    이렇게 변환하면 lubuntu-15.10-desktop-amd64.dmg 파일이 생성된다. 이제 live usb를 연결하고 해당 usb의 경로를 다음 명령으로 확인한다.

    $ diskutil list
    

    출력 결과에서 용량이나 명칭으로 드라이버를 확인한다. 만약 마운트가 되어 있는 상태라면 diskutil unmountDisk /dev/diskN으로 마운트를 해제한다.

    $ diskutil unmountDisk /dev/disk3
    

    이제 usb에 이미지를 덮어 쓴다.

    $ sudo dd if=/path/to/lubuntu-15.10-desktop-amd64.dmg of=/dev/rdisk3 bs=1m
    

    입력이 모두 완료되면 맥에서 드라이브를 읽을 수 없다는 오류가 뜨는데 정상이며 Eject를 누르면 과정이 끝난다.

    백업 및 복원 과정

    이제 usb를 사용해서 부팅을 한다. usb로 부팅이 되지 않는다면 BIOS에서 부팅 순위를 확인한다. USB를 꼽고 부팅을 하면 Try Lubuntu 메시지를 선택해서 lubuntu를 켠다.

    마우스와 키보드가 잘 작동하면 다음 단계로 넘어가면 된다. 블루투스 마우스의 경우에는 Win + R 키로 실행 창을 띄운 후, blueman-manager를 입력해서 실행한다. 그러면 관리자 창이 뜨는데 탭 키와 방향키를 잘 사용해서 마우스를 연결하면 된다.

    마우스가 잘 되면 네트워크 관리자에서 wifi를 연결한다. 시작 상태 막대 끝에 아이콘을 활용한다. 그리고 exfat을 사용할 수 있도록 패키지를 설치한다.

    $ sudo apt-get install exfat-utils exfat-fuse
    

    백업할 드라이브 경로를 디스크 목록에서 찾는다.

    $ sudo fdisk -l
    

    이제 컴퓨터에 이미지 저장용 USB를 꼽는다. 앞서 dd와 같이 백업할 드라이브를 지정하고 저장할 경로를 지정하면 이미지로 백업하게 된다. 현재 저장되어 있는 OS가 무엇인지 상관없이 전체를 백업하게 된다.

    $ sudo dd if=/dev/sdb of=/media/lubuntu/backup/backup.img bs=1m
    

    복원은 반대로 하면 된다. 대신, 이미지를 덮기 전에 마운트 해제가 정상적으로 되어 있는지 확인한다. 숫자를 착각하거나 경로를 잘못 읽어서 엉뚱한 드라이버를 덮지 않도록 조심한다.

    $ sudo mount # 명령으로 마운트를 확인, 마운트되어 있다면 아래 명령으로 언마운트
    $ sudo umount /media/lubuntu/main
    $ sudo dd if=/media/lubuntu/backup/backup.img of=/dev/sdb bs=1m
    

    드라이브를 통채로 떴기 때문에 MBP나 UEFI 같은건 따로 안잡아줘도 되는 것 같다.

    진행 상황 확인하기

    dd 진행 상황을 확인하기 위해서는 status 플래그를 넣으면 된다는데 dd 버전마다 달라서 되는 경우도 있고 안되는 경우도 있다. pv를 설치하는 방법이 간편하다.

    $ sudo apt-get install pv
    $ sudo -i # 귀찮으니 root로 전환
    $ dd if=/media/lubuntu/backup/backup.img | pv | dd of=/dev/sdb bs=1m
    

    지난 21일 Weird Developer Melbourne 밋업이 있었다. 3회차인 이번 밋업은 라이트닝 토크 형식으로 진행되었고 그 중 한 꼭지를 맡아 C# 초보가 C# 패키지를 만드는 방법 주제로 발표를 했다.

    C# 스터디에 참여한 이후에 윈도 환경에서 작업할 일이 있으면 C#으로 코드를 작성해서 사용하기 시작했다. 하지만 업무에서 사용하는 기능은 한정적인데다 의도적으로 관심을 갖고 꾸준히 해야 실력이 느는데 코드는 커져가고, 배운 밑천은 짧고, 유연하고도 강력한 코드를 만들고 싶다는 생각을 계속 하고 있었지만 실천에 옮기질 못하고 있었다.

    얼마 전 저스틴님과 함께 바베큐를 하면서 이 얘기를 했었는데 “고민하지 않고 뭐든 만드는 것이 더 중요하다”는 조언을 해주셨다. 말씀을 듣고 그냥 하면 되는걸 또 너무 망설이기만 했구나 생각이 들어서 실천에 옮겼다. 특별하게 기술적으로 뛰어난 라이브러리를 만들거나 한 것은 아니지만 생각만 하고 앉아있다가 행동으로 옮기는 일을 시작한 계기와 경험이 좋아서 발표로 준비하게 되었다.

    발표 자료는 다음과 같다.

    발표는 다음 같은 내용이 포함되었다.

    • MonoDevelop에서 간단한 예제 코드 시연
    • 라이브러리 작성하면서 배운 것
    • GitHub
    • Nuget 패키지
    • AppVeyor 설정

    라이트닝 토크라서 이 주제가 괜찮지 않을까 생각했지만 다른 분들은 더 심도있는 주제를 많이 다뤄서 쉬어가는 코너 정도 느낌이 되었던 것 같다. 시간을 짧게 한다고 좀 더 설명할 부분을 그냥 넘어가거나 보여줄 페이지를 다 보여주지 못했던 점도 아쉽다.

    발표 이후로도 계속 시간을 내서 라이브러리도 다듬고 C# 공부도 부지런히 해야겠다는 생각을 했다. (아직도 갈 길이 멀다!) 학습에서 유익했던 자료와 보고 있는/볼 예정인 자료를 참고로 남긴다.

    • [C# Fundamentals for Absolute Beginners

    ]6 MVA 강의로 C# 기초와 VS 사용 방법을 배울 수 있음. 최근 리뉴얼 한듯.

    In Weird Developer Melbourne! Thanks @justinchronicles

    지금까지 git을 숱하게 사용했지만 한글 파일명은 문제가 생긴다는 사실을 이제야 알았다.

    다음처럼 core.quotepath를 끄면 commit, status 등에서 한글 출력이 정상으로 돌아온다. 이 설정은 일반적이지 않은 문자를 탈출문자로 처리하는 기능을 수행한다. 그래서 한글 앞에 탈출 문자를 붙인 탓에 이런 문제가 발생했다.

    git config --global core.quotepath false
    

    관련 링크

    올해부터 호주 멜버른에서 IT 개발 직군에 종사하는 한국어 구사자를 위한 Weird Developer Melbourne이 운영되고 있다. 2월 16일 밋업에 발표했던 자료인데 정리해서 올린다고 하고 두 달이나 지나서야 올리게 되었다.

    dnx 대신 dotnet으로 변경한다는 이야기가 한참 있었는데 그 이후로 follow up 하지 못했다. 아래 내용은 발표 당시를 기준으로 한 환경 설정이다. 발표에서 Entity Framework을 사용하기 위해 sqlite3도 포함되어 있다.

    Vagrantfile

    # -*- mode: ruby -*-
    # vi: set ft=ruby :
    Vagrant.configure(2) do |config|
      config.vm.box = "ubuntu/vivid64"
      config.vm.network "forwarded_port", guest: 5000, host: 8080
      config.vm.network "public_network"
      #config.vm.network :private_network, id: "192.168.33.20"
      config.vm.synced_folder ".", "/home/vagrant/weirdnote"
      config.vm.provider "virtualbox" do |vb|
        vb.memory = "1024"
      end
      config.vm.provision "shell", path: "tools/vagrant/provision.sh"
    end
    

    의존 패키지 설치

    $ sudo apt-get update
    # DNX prerequisites
    $ sudo apt-get install -y unzip curl libunwind8 gettext libssl-dev \
      libcurl4-openssl-dev zlib1g libicu-dev uuid-dev
    # install libuv for KestrelHttpServer
    $ sudo apt-get install -y automake libtool
    # sqlite3
    $ sudo apt-get install libsqlite3-dev
    $ curl -sSL https://github.com/libuv/libuv/archive/v1.4.2.tar.gz \
     | sudo tar zxfv - -C /usr/local/src
    $ cd /usr/local/src/libuv-1.4.2
    $ sudo sh autogen.sh
    $ sudo ./configure
    $ sudo make
    $ sudo make install
    $ sudo rm -rf /usr/local/src/libuv-1.4.2 && cd ~/
    $ sudo ldconfig
    

    DNVM 설치

    # install DNVM
    $ curl -sSL https://raw.githubusercontent.com/aspnet/Home/dev/dnvminstall.sh \
     | DNX_BRANCH=dev sh && source ~/.dnx/dnvm/dnvm.sh
    # dnvm set as coreclr
    $ dnvm upgrade -r coreclr
    

    NodeJS 설치

    # nvm install
    $ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.30.2/install.sh \
    | bash
    $ source ~/.nvm/nvm.sh
    # install node
    $ nvm install v5.5.0
    $ nvm alias default v5.5.0
    # node related
    $ npm install -g yo bower grunt-cli gulp
    $ npm install -g generator-aspnet
    

    이전에도 iterm을 설치했었지만 키맵이 영 익숙해지지 않고 기본 터미널과 맞추려니 이것저것 찾아보는게 귀찮아서 계속 터미널을 사용하고 있었다. neovim을 설치하는 차에 iterm3 베타가 나왔다는 얘기가 생각나서 iterm도 설치했다.

    Opt + 방향키로 단어 사이 이동을 종종 하는 편인데 iterm 키맵엔 이 설정이 포함되어 있지만 쉘에서 추가적인 설정이 필요하다. 구글링 해보면 ~/.inputrc에 다음과 같이 추가하면 동작한다고 하는데 이 방법은 bash를 사용하는 경우에 해당한다.

    "\e\e[D": backward-word
    "\e\e[C": forward-word
    

    zsh의 경우는 ~/.zshrc에 다음처럼 추가하면 된다.

    bindkey "\e\e[D" backward-word
    bindkey "\e\e[C" forward-word
    

    추가. 설정할 때는 몰랐는데 iTerms의 키맵이 어떻게 되어 있는가에 따라 다르다. junho85님의 경우는 아래 키맵으로 설정했다고 한다.

    bindkey -e
    bindkey "^[[1;9C" forward-word
    bindkey "^[[1;9D" backward-word
    

    spring의 gradle로 프로젝트 시작하기를 따라하며 정리한 글이다.

    먼저 brew로 java와 의존성 및 빌드 관리/자동화 도구인 gradle을 설치한다.

    $ brew tap caskroom/cask
    $ brew install brew-cask
    $ brew cask install java
    $ brew install gradle
    

    문제없이 설치되었다면 버전 정보를 출력한다.

    $ gradle -v
    

    gradle로 프로젝트를 초기화한다.

    $ gradle init
    

    초기화하면 기본적으로 gradle wrapper를 생성해주는데 이 스크립트는 gradle이 없는 환경에서도 gradle을 사용할 수 있도록 돕는 스크립트다.

    예제 클래스를 먼저 작성한다.

    $ mkdir -p src/main/java/hello
    
    // src/main/java/hello/HelloWorld.java
    
    package hello;
    
    public class HelloWorld {
      public static void main(String[] args) {
        Greeter greeter = new Greeter();
        System.out.println(greeter.sayHello());
      }
    }
    
    // src/main/java/hello/Greeter.java
    
    package hello;
    
    public class Greeter {
      public String sayHello() {
        return "Hello world!";
      }
    }
    

    빌드와 관련한 모든 설정은 build.gradle에 담겨 있다. 빌드를 위해 다음 내용을 build.gradle에 추가한다.

    apply plugin: 'java'
    

    그리고 빌드를 하면 build 디렉토리를 생성하고 빌드를 진행한다.

    $ gradle build
    

    아래 내용을 추가해서 어플리케이션을 직접 구동할 수 있다.

    apply plugin: 'application'
    mainClassName = 'hello.HelloWorld'
    

    gradle을 설치한 환경에서는 gradle을 사용해도 되겠지만 다음과 같이 앞에서 생성한 wrapper를 사용해서 구동할 수 있다.

    $ ./gradlew run
    

    다음은 튜토리얼에서 최종적으로 작성하게 되는 gradle 파일이다.

    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'application'
    
    mainClassName = 'hello.HelloWorld'
    
    // tag::repositories[]
    // 서드파티 라이브러리의 소스 출처를 추가한다
    repositories {
        mavenCentral()
    }
    // end::repositories[]
    
    // tag::jar[]
    // 빌드에서 jar를 생성할 때 메타를 추가한다
    jar {
        baseName = 'gs-gradle'
        version =  '0.1.0'
    }
    // end::jar[]
    
    // tag::dependencies[]
    // 버전 의존성을 추가한다
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    
    // 의존 라이브러리를 추가한다
    dependencies {
        compile "joda-time:joda-time:2.2"
    }
    // end::dependencies[]
    
    // tag::wrapper[]
    // wrapper로 설치할 gradle version을 정한다
    task wrapper(type: Wrapper) {
        gradleVersion = '2.3'
    }
    // end::wrapper[]
    

    최근에 구입한 Dell 노트북에 조금이라도 가볍게 사용해보려고 Lubuntu를 설치해서 사용하고 있다. 트랙패드가 예전에 비해 많이 나아지긴 했지만 아무래도 맥북에서 사용하던 것과는 많이 달라서 좀 더 키보드 친화적인 환경을 꾸려야겠다는 생각이 들었다. 그러던 중 tmux와 다시 친해질 기회인 것 같아서 tmux를 설치하게 되었다.

    어제 tmux 이야기를 트위터에 올렸더니 ujuc님이 powerline이란 멋진 tmux 플러그인을 소개해주시고, 사용하는 rc 파일을 공유해주셨다.

    만약 tmux 플러그인에 관심이 있다면 이 페이지가 도움이 된다.

    그 외에도 간단하게 설치할 수 있는 것도 많이 보였다.

    구글 검색해보면 이것저것 유용한 도구가 많이 나온다.

    tmux 설정 되돌리기

    tmux.conf 재미있는 점이 default 파일이 존재하지 않는다는 점이다. 설정 하나를 변경하면 기존 설정을 알지 못하는 이상 다시 기본 설정으로 돌아갈 수가 없다. 그 눈 아픈 기본값 초록색 상태 막대로 한번에 돌아갈 방법이 없다는 뜻이다.

    그래서 tmux 기본 설정을 어딘가 추출해서 보관해두면 다시 돌아오는데 편리하다. 현재 tmux에 설정된 값은 다음 명령어로 추출할 수 있다.

    $ tmux show -g | sed 's/^/set-option -g /' > ~/.tmux.current.conf
    

    구글링 해보면 멋지게 꾸며진 tmux.conf를 많이 볼 수 있다. 나처럼 설정을 잘 모르고 적용했다가 명령을 시작하기 위해 사용하는 프리픽스인 Ctrl + b를 이상한걸로 변경해서 종료도 못하고 오고가도 못하는 상황을 마주할 수도 있으니 꼭 기본 설정을 추출해두자.

    tmux.conf를 적용하는 명령은 source-file이다.

    $ tmux source-file ~/.tmux.current.conf
    

    직접 설정 변경하기

    내 경우는 터미널 폰트를 비트맵으로 사용하고 있어 앞서 powerline을 적용하니 대다수가 깨져 이쁘게 적용되질 않았다. 게다가 사양 탓인지 좀 느려지는 기분이라서 간단하게 색상 바꾸고 필요한 것만 설치하기로 했다.

    tmux에서 가장 필요했던 부분은 배터리 잔량 표시와 일자/시간 표시였다. 일자/시간은 기본적으로 가능한 부분이라 배터리 잔량 표시는 다음 프로그램을 설치했다.

    아쉽게도 잔량 표시 그림은 그려지지 않지만 수치가 나오니 그럭저럭 만족하고 있다.

    기분 전환 겸 상태 막대 색상도 초록에서 연한 회색(colour235)로 변경했다. 사용할 수 있는 색상은 다음 스크립트로 확인할 수 있다.

    for i in {0..255} ; do
        printf "\x1b[38;5;${i}mcolour${i}\n"
    done
    

    누가 이 결과를 보기 좋게 github에 올려뒀다.

    원하는 색상이 나오지 않을 때

    이 색상 설정은 256color 모드로 실행하지 않은 터미널에서는 동작하지 않는다. 색상이 적용되지 않는다면 다음 설정을 참고하자.

    # .bashrc or .zshrc 에 추가
    export TERM=xterm-256color
    alias tmux="tmux -2"
    
    # .tmux.conf 에 추가
    set -g default-terminal "screen-256color"
    

    OS X의 터미널은 기본적으로 256color로 설정되어 있다.


    tmux의 기본적인 기능은 예전 요약했던 내용이나 nanhapark님의 포스트를 참고하면 되겠다. 물론 이런 요약본도 tmux.conf 한방에 모두 변경될 수 있어서 tmux.conf를 조심하자는 이상한 결론을 내려본다.

    사이드 프로젝트에서 Express를 오랜 기간 사용했었는데 hapi 가 좋다는 얘기를 듣고는 hapi를 많이 사용해왔다. Hapi도 단순하긴 하지만 “설정만 넣으면 되는” 단순함이라서 설정에 들어가는 수고가 꽤 컸다. 최근에는 토이 프로젝트에서 API를 작성하는데 에러 발생 여부에 따라서 {"ok": true} 하나 넣어주는 작업에 오만가지 코드를 작성해야 했다. express와 다르게 미들웨어에서 request, response에 접근할 수 있는 포인트가 워낙에 많아 더 복잡하게 느껴졌다. 그러던 중 예전에 잠시 비교글로 봤던 koa를 살펴봤는데 지금 필요한 상황에 맞는 것 같아 koa로 다시 코드를 작성했고 마음에 드는 구석이 많아서 간단한 소개를 작성한다.

    Koa는 ES2015의 문법 중 하나인 제너레이터를 적극적으로 활용하고 있는 웹 프레임워크다. 모든 요청과 처리를 제너레이터를 활용해 파이프라인을 만드는 것이 특징이며 그 덕분에 깔끔한 async 코드를 손쉽게 작성할 수 있다. Express 만큼은 아니더라도 다양한 라이브러리를 제공하고 있고, express의 라이브러리나 미들웨어도 thenify나 co로 변환해서 활용할 수 있을 만큼 확장성이 높다.

    이 포스트는 제너레이터를 먼저 살펴보고, 제너레이터를 유용하게 사용할 수 있는 co를 살펴본 후, KoaJS를 간단하게 살펴보는 것으로 마무리한다.


    제너레이터 Generator

    다른 언어에도 이미 존재하고 있기 때문에 크게 특별한 기능은 아니지만 ES6에서의 구현을 간단히 정리하려고 한다.

    일반적인 함수의 경우, 매 실행마다 같은 흐름으로 모든 코드를 실행하지만 Generator 함수는 실행 중간에서 값을 반환할 수 있고, 다른 작업을 처리한 후에 다시 그 위치에서 코드를 시작할 수 있다. 이 제너레이터는 반복 함수 iterator를 next()로 제공하고 결과를 value로, 진행 상황을 done으로 확인할 수 있다.

    구구단을 제너레이터로 작성하면 다음과 같다.

    function* nTimesTable(n) {
      for(var i = 1; i <= 9; i++) yield n * i;
    }
    

    제너레이터는 위와 같이 function* fnName(){} 식으로 *을 넣어 선언한다. 익명 함수의 경우도 function*(){} 식으로 선언한다.

    이제 이터레이터(iterator)를 nineTimesTable에 반환 받는다.

    var nineTimesTable = nTimesTable(9);
    

    이터레이터는 next()를 통해 실행할 수 있다. 이 함수로 중단한 위치의 결과가 반환된다.

    var result = nineTimesTable.next();
    console.log(result); // { value: 9, done: false }
    result = nineTimesTable.next();
    console.log(result); // { value: 18, done: false }
    result = nineTimesTable.next();
    console.log(result); // { value: 27, done: false }
    
    // keep calling...
    
    result = nineTimesTable.next();
    console.log(result); // { value: 72, done: false }
    result = nineTimesTable.next();
    console.log(result); // { value: 81, done: false }
    result = nineTimesTable.next();
    console.log(result); // { value: undefined, done: true }
    

    매 반복 실행에서 value를 반환하지만 동시에 done으로 해당 함수가 yield 결과 없이 종료되었는지 확인할 수 있다. 마지막에 별도의 return 값이 없기 때문에 valueundefined가 된다.

    이런 이터레이터의 반환 특징을 이용하면 다음과 같이 iterator를 호출하는 함수를 작성할 수 있다.

    function caller(iter) {
      var result, value;
      while(result = iter.next()) {
        if(result.done) break;
        value = result.value || value;
      }
      return value;
    }
    
    var result = caller(nTimesTable(3));
    console.log(result); // 27
    

    donetrue를 반환할 때까지 해당 이터레이터를 실행해 결과값을 가져오는 caller를 작성했다. 만약 매 반복에서 특정 함수를 실행하고 싶다면 다음처럼 작성할 수 있다. 앞서 작성한 nTimesTable 함수가 더 많은 내용을 반환하도록 수정했다.

    function * nTimesTable(n) {
      for(var i = 1; i <= 9; i++) yield { n: n, i: i, result: n * i };
    }
    
    function caller(iter, func) {
      var result, value;
      while(result = iter.next()) {
        if(result.done) break;
        value = result.value || value;
        if(func) func(value);
      }
      return value;
    }
    
    caller(nTimesTable(3), value => {
      console.log('%d x %d = %d', value.n, value.i, value.result);
    });
    

    앞서 작성한 caller는 제너레이터 내의 yield에 대해서는 처리를 하지 못한다. 제너레이터에서 이터레이터를 반환하고 진행을 중단했을 때 해당 이터레이터를 처리해서 다시 반환해야 한다. 결과를 넣고 다시 진행할 수 있도록 작성해야 하는 것이다.

    function* getAnimalInCage() {
      yield "Wombat";
      yield "Koala";
      return "Kangaroo";
    }
    
    function* Cage() {
      var cageAnimals = getAnimalInCage();
    
      var first = yield cageAnimals;
      var second = yield cageAnimals;
      var third = yield cageAnimals;
    
      console.log(first, second, third);
    }
    

    Cage 제너레이터를 실행하면 yield를 3번 사용했기 때문에 최종 console.log가 출력하는 결과를 보기까지 4번에 걸쳐 실행된다.

    var cage = Cage();
    var firstStop = cage.next();
    // {value: iterator, done: false}
    

    첫 번째 yield 결과가 firstStop에 저장되었다. cageAnimals는 위에서 코드에서와 같이 getAnimalInCage 제너레이터가 생성한 이터레이터다. 이 이터레이터에 next() 메소드로 값을 받은 후, 그 값을 다시 first 변수에 다음과 같이 반환한다.

    var firstAnimal = firstStop.value.next();
    // firstAnimal: {value: "Wombat", done: false}
    var secondStop = cage.next(firstAnimal.value);
    

    next의 인자값으로 첫 결과인 Wombat을 넣었다. 이전에 멈췄던 위치인 첫 번째 yield로 돌아가 함수 내 first에는 Wombat이 저장된다. 나머지도 동일하게 진행된다.

    var secondAnimal = secondStop.value.next();
    // secondAnimal: { value: 'Koala', done: false }
    
    var thirdStop = cage.next(secondAnimal.value);
    var thirdAnimal = thirdStop.value.next();
    // thirdAnimal: { value: 'Kangaroo', done: true }
    
    var lastStop = cage.next(thirdAnimal.value);
    
    // Wombat Koala Kangaroo
    

    마지막 Kangaroo는 yield가 아닌 return이기 때문에 done이 true를 반환한다. 앞서 직접 호출해서 확인한 코드는 반환하는 값이나 호출하는 형태가 일정한 것을 볼 수 있다. 즉 재사용 가능한 형태로 만들 수 있다는 의미다.

    다음은 catchEscapedAnimal()getTodaysZookeeper() 함수를 이용한 Zoo 제너레이터 예시다.

    function catchEscapedAnimal() {
      return function(done) {
        setTimeout(function() {
          done(null, {name: 'Kuma', type: 'Bear'});
        }, 1000);
      };
    }
    
    function* getTodaysZookeeper() {
      yield {status: 'loading'};
      return {status: 'loaded', name: 'Edward'};
    }
    
    function* Zoo() {
      var animal = yield catchEscapedAnimal();
      var zookeeper = yield getTodaysZookeeper();
    
      console.log('%s catches by %s', animal.name, zookeeper.name);
    }
    

    catchEscapedAnimal()은 ajax를 사용하는 경우를 가정해서 setTimeout을 이용해 콜백을 호출하는 형태로 작성되었다. getTodaysZookeeper()는 일반적인 제너레이터 함수로 첫 호출에는 loading을, 두번째 호출에서 최종 값을 전송한다. Zoo도 앞에서 본 Cage처럼, 중간에 yield를 사용한다. 이 함수를 처리하기 위한 compose 함수는 다음과 같다.

    function compose(iter, value, next) {
      var result = iter.next(value);
      if(result.done) return next ? next(value) : value;
      else if(typeof result.value == 'function') {
        return result.value(function(err, data) {
          if(err) throw err;
          compose(iter, data);
        });
      } else if(typeof result.value.next == 'function') {
        var _iter = iter;
        next = function(result){
          compose(_iter, result);
        };
        iter = result.value;
        result = iter.next();
      }
      return compose(iter, result.value, next);
    }
    

    compose 함수는 다음과 같은 경우의 수를 다룬다.

    • yield 된 값이 함수일 때, 호출 체인을 연결할 수 있도록 next 함수를 넘겨줌 (기존 callback 방식)
    • yield 된 값이 이터레이터일 때, 이터레이터가 done을 반환할 때까지 호출한 후 최종 값을 반환
    • 그 외의 결과를 반환할 때, 해당 값을 이터레이터에 넣고 다시 compose를 호출
    • 이터레이터가 종료(done == true)되었을 때, next 함수가 있다면 해당 함수로 호출을 진행하고 없으면 최종 값을 반환하고 종료

    이 함수를 이용한 결과는 다음과 같다. setTimeout()에 의해 중간 지연이 진행되는 부분도 확인할 수 있다.

    compose(Zoo());
    // Kuma catches by Edward
    

    제너레이터를 코루틴으로, co

    나름 잘 동작하지만 흐름을 보기 위해서 만든 함수라서 허술한 부분이 많다. 이런 부분에서 사용할 수 있는 것이 바로 co다. co는 제너레이터를 코루틴처럼 사용할 수 있도록 돕는 라이브러리로 앞서 작성했던 compose 함수와 같은 역할을 한다.

    var co = require('co');
    co(Zoo());
    // Kuma catches by Edward
    

    이 라이브러리는 내부적으로 Promise 패턴을 사용하고 있어서 callback이든 Promise든 제너레이터든 모두 잘 처리한다. 실제로 제너레이터를 사용하고 싶다면 이 라이브러리를 사용하는 것이 큰 도움이 된다.

    Koa

    Koa는 앞서 이야기한 co 라이브러리를 기본적으로 적용하고 있는 HTTP 미들웨어 라이브러리로 경량에 간단한 기능을 제공하는 것을 특징으로 한다. 제너레이터를 기본적으로 사용할 수 있어서 앞서 배운 내용을 손쉽게 적용할 수 있다.

    코드를 작성하기에 앞서 간단하게 koa를 설치한다.

    $ npm install --save koa
    

    Hello World를 작성하면 다음과 같다.

    var koa = require('koa');
    var app = koa();
    
    app.use(function* () {
      this.body = {"message": "Hello World"};
    });
    
    app.listen(3000);
    

    이제 http://localhost:3000에 접속하면 해당 json이 출력되는 것을 확인할 수 있다.

    앞서 작성한 코드도 포함해보자.

    var koa = require('koa');
    var app = koa();
    
    function catchEscapedAnimal() {
      return function(done) {
        setTimeout(function() {
          done(null, {name: 'Kuma', type: 'Bear'});
        }, 50);
      };
    }
    
    function* getTodaysZookeeper() {
      yield {status: 'loading'};
      return {status: 'loaded', name: 'Edward'};
    }
    
    function* Zoo() {
      var animal = yield catchEscapedAnimal();
      var zookeeper = yield getTodaysZookeeper();
    
      this.body = { message: animal.name + ' catches by ' + zookeeper.name };
    }
    
    app.use(Zoo);
    app.listen(3000);
    

    Koa의 모든 추가 기능은 미들웨어 구조로 제너레이터를 통해 작성하게 된다. callback은 물론 Promise 패턴도 더 깔끔하게 사용할 수 있다.

    요청과 응답은 모두 this에 주입되서 전달되고 흐름은 첫 인자에 next를 추가해 제어할 수 있다. 요청에 대한 응답 내용이 있으면 ok를 추가해보자.

    app.use(function* (next) {
      yield next;
      if(this.body) {
        this.body.ok = true;
      } else {
        this.body = { ok : false };
      }
    });
    

    다음과 같은 방식으로 토큰 검증도 가능하다.

    app.use(function* (next) {
      var requestToken = this.request.get("Authorization");
      var accessToken = yield AccessTokensModel.findAccessTokenAsync(token);
      if(accessToken) {
        yield next;
      } else {
        this.body = { error: 'invalid_token' };
      }
    });
    

    세부적인 내용은 koa 웹페이지에서 다루고 있다. 단순하고 간편한 기능을 원한다면 꼭 살펴보자. 실제 사용하게 될 때는 koa-bodyparser, koa-router와 같은 패키지를 같이 사용하게 된다. 패키지 목록은 koa 위키에서 확인할 수 있다.

    제너레이터도 충분히 편한 기능이지만 koa는 현재 await/async 문법을 지원하기 위한 다음 버전 개발이 진행되고 있다. 더 가독성도 높고 다른 언어에서 이미 구현되어 널리 사용되고 있는 문법이라 더 기대된다.


    더 읽을 거리

    색상을 바꿔요

    눈에 편한 색상을 골라보세요 :)

    Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.