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

    2012년 3월, 덜컥 호주행 비행기를 타고 멜버른에 도착한 그 날이 아직도 생생하다. 캐리어를 밀고 백팩커에 체크인 하던 나를 기억해보면 그 때의 나는 무슨 생각으로 이런 일을 저질렀을까, 그런 생각이 든다. 그렇게 호주 생활을 시작한지 만 4년이 조금 넘은 지금, 2016년 6월, 호주 영주권을 갖게 되었다.

    겁 없이 올 수 있던 이유

    당시에는 엄청 준비하고 나왔다고 생각했지만 지금 봐서는 참 도전적이었고 나쁘게 말하면 많이 안일하게 준비했다. 영어도 부족했고 현지 사정에도 밝지 않았다. 현지 취업 사이트에서 취업 공고 보고 내 이력서와 맞는 자리가 얼마나 있을지 알아본 정도였다. 그래서 4년이란 불안정한 기간동안 마음 고생도 심했었다. 즐길 수 있던 시간도 있었지만 그만큼 자기 관리가 필요했다. 타지에서 오래 지낸 경험이 없어 내게 쉬운 일은 아니였다. 감사하게도 여기에서 만난 분들의 도움을 많이 받을 수 있었다.

    지금은 탈조선에 성공한 것을 축하한다는 인사도 받긴 하지만 내가 나올 즈음인 4년 전까지만 해도 이토록 탈조선에 대한 담론이 많지 않았다. 게다가 내 삶에 직접적인 영향을 준 한국 경험의 시간은 4년 전을 기준으로 멈춰 있다. 간접적으로 듣는 경제 사정이나 청년 계층의 취업난은 실제가 어느 정도 수준인지 가늠하기 어려울 정도로 심각하게 느껴진다. 나는 엄청난 의지로 꼭 탈출하고 말리라 정도로 생각하고 나온 것이 아니라서 요즘 해외로 취업하고 싶은데 어떻게 하면 되냐는 질문에 다소 다른 온도의 답변을 하게 되는 것 같다.

    내가 다니던 대학 학비는 학기에 170만원 정도 하는 지방 국립대였고 집에서 살았기 때문에 자취를 할 일도 없었다. 군 전역 후에 1년 간 회사를 다니며 모은 적금을 정리하고 호주 올 준비를 한 다음에 300만원을 환전해서 호주로 넘어왔다. 부양 가족도, 갚아야 하는 학자금 대출도 없었다. 만약 학교 학비가 더 비쌌더라면 앞에 낸 비용이 아까워서라도 학교를 끝내야 한다는 강박이 생기지 않았을까 생각도 든다. 워킹홀리데이는 그나마 가장 적은 밑천으로 시작할 수 있는 선택지다. 하지만 이런 말을 어디서 쉽게 하지 않았던 이유는 이런 내 배경으로 쉽게 느끼는 것은 아닌가 고민했기 때문이다. 다녀야 할 학교도 있고 학자금 대출까지 있다면 워홀 커뮤니티에서 흔히 하는 말처럼 경험, 영어, 돈 중 하나 내지 둘 챙기는 것 외에는 결정을 내리기 쉽지 않을 것 같다. 물론 돈이 더 있다면 더 좋은 선택지가 많겠지만 말이다.

    지금 보면 참 어린 생각이지만 프로그래밍으로 취업할 수 있을거란 자신감이 있었다. 어릴 때부터 재미로 만들던 웹사이트인데 이 일로 돈을 번다는 것을 군 입대 전에야 알았다. 전역하고 나서는 지방 기업이지만 웹에이전시에서 개발팀장으로 근무하면서 한참 자신감이 충만해졌다. 같은 일을 한다면 전혀 두려울 것이 없을 정도로 알게 되니 겁이 없어졌다. 한국 이 촌구석에도 이렇게 일이 있는데 호주라고 없겠나 싶은 생각도 들었다.

    오기 전에도, 오고 나서도 흔하지 않은 케이스였고 좋은 이야기를 들은 기억이 손에 꼽았다. 그래서 지금까지 지낼 수 있었던 점에 더 감사하게 된다.

    호주에 도착해서

    지인이 도와줘서 미리 만들어온 커버레터와 준비해온 이력서를 들고 지원할 수 있는 모든 포지션에 지원했다. 열린 포지션이라면 어디든지 지원 했지만 리쿠르터를 거치는 경우에는 아무래도 영어가 약해서 면접까지 가는 일이 쉽지 않았다. 이런 일은 예상했지만 실제로 경험을 하고 반복하다보면 기운빠지는 일이긴 하다. 그래도 주변에서는 리쿠르터가 아예 전화를 하지 않는 경우도 많다고 그래서 전화 오고 인터뷰가 잡힌다는 사실 자체에 감사했다. 많은 전화 덕분에 오히려 더 빠르게 전화에 익숙해질 수 있지 않았나 생각이 든다.

    그러던 중에 급한 프로젝트에 투입된 적도 있었다. 2주 정도의 짧은 기간이었지만 좋은 레퍼런스를 얻을 수 있었다. 이 레퍼런스를 가지고 부지런히 자리를 찾으려고 노력했다. 이때 만난 저스틴님께도 조언을 많이 들을 수 있었고 지금까지도 항상 좋은 말씀을 해주셔서 감사할 따름이다.

    리쿠르터를 통과하는 일이 아무래도 적다보니 콜드메일도 정말 많이 보냈다. 멜번 지역에 있는 회사를 구글 맵스에서 검색해서 일일이 웹사이트를 확인했다. 채용 페이지가 없을 때는 문의 이메일로 이력서와 커버레터를 보내기도 했다. 인터뷰도 더 많이 잡을 수 있었고 그렇게 지금 회사에 다니게 되었다.

    호주에 지내는 내내 호주에서 아무 일도 못해보고 한국을 돌아갔다면 어땠을까 하는 궁금증은 늘 따라다녔다. 제주에서 임용고시를 준비하며 씨름하고 있었을까, 서울에서 계속 웹개발을 하고 있었을까.

    비자 문제

    해외 체류에 있어서 비자는 늘 문제다. 물론 학력이 좋거나 많은 연봉을 받는 사람이라면 비자는 전혀 문제가 되지 않는다. 모셔가야 하는 사람 발목은 안잡는다. 그 외의 경우는 내 자신이 왜 비자를 받아아 하는지 설득해야 한다. 작게 보면 회사의 상사에게 그래야 하고 크게 보면 호주 정부에게 내 비자의 타당성을 서류로 검증받아야 하는 것이다. 이렇게 말은 쉽지만 체류하는 사람마다 비자와 관련한 에피소드는 다들 하나는 갖고 있을 정도다.

    비자 발급 비용도 적지 않다. 나는 고맙게도 모든 비용을 회사에서 처리해줘 부담없이 잘 받을 수 있었다. 준비해야 할 서류가 크게 복잡하게 느껴지지 않아 대리인을 고용하지 않았다. 그리고 호주 이민성에서 제공하는 체크리스트를 보며 준비 서류를 챙겼다. 그렇게 워킹홀리데이 비자에서 Subclass 457 비자로 전환 후 2년 반을 지내고 영주 비자인 ENS 비자를 신청했다.

    비자를 신청하기 전에 요건을 충족하는 것이 1차 관문이고 비자를 신청하고 처리되기까지 기다리는 일이 2차, 담당자가 배정되어 통과되는 일이 마지막 관문이다. 처리를 기다리는 기간이 기본적으로 5개월 이상인데 이 기간 동안 별 생각이 다 든다. 게다가 까탈스러운 담당자를 만난다면 정말 정신을 붙잡기가 쉽지 않다. 내 경우에도 꼼꼼한 담당자를 만나서 번거로운 일이 있었지만 다행히 큰 문제가 되진 않아 감사했던 기억이 난다.

    영주권은 끝이 아니라 시작

    마치 고등학생 때 수능 보고 대학만 가면 모든 일이 끝날 것 같지만 더 많은 선택지와 삶의 방향 앞에서 혼란을 겪는 것과 비슷한 것 같다. 영주권을 받고 나서 기쁜 마음도 분명 있지만 이제는 더 이상 내 비자 상태를 탓하며 아무 일도 안하며 손놓고 있을 수 없다는 생각이 들어 괜스레 부담감도 생긴다. 그런 부담감도 있고 앞으로 어떤 일을 해야 할 지 뚜렷하게 생각해보지 않고 당장 앞에 있는 일에만 바쁘게 지냈는데 막상 그 날이 와서 어떻게 해야 할지 잘 모르겠다고 할까. 그래도 기쁨과 감사함으로 앞으로 시간을 잘 준비해야겠다는 생각을 한다.

    호주에서의 삶부터 지금까지 지내온 시간은 나에게 너무나도 새로운 경험이었고 평생 가지고 갈 내 인생의 밑천이 되었다. 호주에서 어떤 일이 있었든지 또 다시 다른 나라를 알아보고 도전해보지 않았을까. 지금 내가 갖고 있는 모든 것을 털고 또 새롭게 시작해보겠냐 그러면 그럴꺼라 대답할 것이다. 그 인생의 충격은 또 다시 경험해보고 싶을 정도로 매력적이다.

    다행인지 삶은 여기에 끝이 아니라 도전의 연속이다. 영주권이 큰 고비라고 생각했지만 앞으로도 등정하고 싶은 산도, 올라야 하는 산도 많이 보인다. 그러면서도 여전히 내 자신에게서 부족한 부분을 많이 보게 된다. 더 나아지기 위해 노력하는 동시에 결여를 부정하거나 괴로워하거나 덮어두지 말고, 있는 그대로를 자연스럽게 받아드릴 수 있는 사람이 되었으면 좋겠다. 그리고 지금까지의 삶에서 갚아야 할 감사가 더 많아졌다. 부지런히 살고 열심히 지내서 도움 주신 모든 분들에게 보답하고 싶고 다른 사람에게 그런 도움을 줄 수 있는 사람이 되도록 노력해야겠다.

    앞으로 어떤 삶을 마주하게 될 지 기대된다.


    다음은 호주 생활과 관련해서 썼던 글이다.

    지난 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

    Interfaces separated from the class implementation in separate projects?를 짧게 번역했다. 이 포스트는 cc-by-sa를 따른다.


    인터페이스는 클래스 구현과 별도의 프로젝트로 분리해야 하나요?

    Tomas Walek의 질문

    현재 중간 규모의 프로젝트를 개발자 3명이서 6개월 넘게 진행하고 있다. 구체적인 구현에서 인터페이스를 분리하자는 결론에 이르렀다. 가장 먼저 인터페이스를 별도의 파일로 보관하기로 했다.

    추가적으로 데이터를 더 분리하기 위해서 인터페이스 .CS 파일과 헬프 클래스 .CS 파일(이 인터페이스를 사용하는 퍼블릭 클래스나 enum 등)을 담은 프로젝트(CSPROJ)를 만들었다. 그리고 팩토리 패턴이나 구체적인 인터페이스 구현, 다른 “워커” 클래스 등을 별도의 프로젝트(CSPROJ)로 만들었다.

    어떤 클래스든 인터페이스를 구현하는 개체를 생성하려면 그 자체만으로 구현하지 않고 인터페이스와 퍼블릭 클래스를 포함하는 첫 번째 프로젝트로 분리한 다음에 해당 프로젝트를 포함하는 방식으로 작성했다.

    이 해결책은 큰 단점이 있다. 어셈블리 수가 2배로 늘게 된다는 점인데 모든 “일반” 프로젝트가 하나의 인터페이스 프로젝트와 하나의 구현 프로젝트를 포함하게 되기 때문이다.

    당신의 추천은 무엇인가? 각각 프로젝트 자체에 인터페이스를 보관하는 것보다 인터페이스 만을 위한 별도의 프로젝트 하나를 갖는 것이 좋은 생각인가?


    Wim Coenen의 답변

    1. **독자적 인터페이스 (Standalone interfaces)**는 프로젝트 나머지와 소통할 필요 없이 사용할 수 있도록 제공하는 목적에서 작성한다. 이런 인터페이스는 단일 항목으로 “인터페이스 어셈블리(interface assembly)”에 넣게 되고 프로젝트 내 모든 어셈블리가 참조할 것이다. ILogger, IFileSystem, IServiceLocator가 전형적인 예시다.
    2. **클래스 결합 인터페이스 (Class coupled interfaces)**는 오직 프로젝트 내의 클래스와 사용하는 맥락에서만 이해가 되는 경우다. 이 인터페이스는 의존성을 갖는 클래스와 동일한 어셈블리에 포함한다.

    예를 들어보자. 도메인 모델이 Banana 클래스를 갖고 있다고 가정한다. 바나나를 IBananaRepository 인터페이스를 통해서 얻을 수 있다면 이 인터페이스는 바나나와 밀접하게 결합된 상황이다. 이 경우에는 바나나에 대해 알지 못하고서는 이 인터페이스를 구현을 한다거나 이 인터페이스를 사용하는 일이 불가능하다. 그러므로 이 인터페이스는 바나나 어셈블리와 함께 위치하는 것이 논리적이다.

    앞 예제는 기술적인 결합이지만 논리적으로 결합하는 경우도 있다. 예를 들면, IFecesThrowingTarget 인터페이스는 Monkey 클래스에 기술적인 연결 고리로 선언되어 있지 않더라도 Monkey 클래스와 함께 사용하는 경우에만 유의미할 수 있다.

    내 답변은 개념에 의존적이지 않으며 클래스가 약간 결합하는 정도는 괜찮다고 생각한다. 모든 구현을 인터페이스 뒤에 숨기는 일을 실수일 것이다. 의존성을 주입하거나 팩토리를 통해 인스턴스를 생성하지 않고 그냥 클래스를 “new 키워드로 생성”하는 것도 괜찮을 수도 있다.

    tmux는 입력하는 내용을 현재 열린 모든 pane에 전달하는 기능을 제공한다. 여러 위치에 있는 내용에 대해서 동일한 작업을 수행해야 하는 경우에 유용하게 사용할 수 있다. 이전부터 기능이 있다는 점은 알고 있었는데 딱히 사용할 일이 없다가 최근 장애 대응 중에 유용하게 사용해서 기록 해둔다.

    tmux 창에서 pane을 열고 다음과 같이 입력한다. on이나 off를 지정하지 않으면 현재 설정과 반대로 토글한다.

    <Ctrl-B>, :
    setw synchronize-panes on
    

    설정을 켠 다음에 입력하면 현재 창에 열린 모든 pane에 동시에 입력되는 것을 확인할 수 있다.

    자주 사용한다면 단축키로 저장해둘 수 있다. .tmux.conf에 다음처럼 설정을 추가한다.

    bind-key y set-window-option synchronize-panes
    

    이제 <Ctrl-B>, y로 간편하게 사용할 수 있다.

    2016-07-13 추가:

    Vue.js 포럼에 한국어 사용자 카테고리가 추가되었고 해당 포럼에서 문서 한국어화를 진행한다고 한다. 이 문서 외 Vue.js에 관심이 있다면 해당 포럼을 확인해보자.


    Vue.js 문서를 살펴보던 중에 Comparison with Other Frameworks 내용이 괜찮아서 짧게 번역했다. Vue.js의 문서답게 기승전vue.js 이긴 하지만 각각의 프레임워크가 어떤 특징이 있고 어떤 주요 이슈가 있는지 잘 정리되었다.


    다른 프레임워크와 vue.js 비교

    Angular

    모두에게 적용될 만한 항목은 아니지만 Angular 대신에 Vue를 선택해야 할 이유가 몇 가지 있다.

    • Vue.js는 API나 디자인 측면에서 Angular에 비해 훨씬 단순하다. 대부분의 내용을 빠르게 배울 수 있어서 생산성이 좋다.
    • Vue.js는 더 유연하면서도 덜 의견지향적인 해결책을 제시한다. 이 특징은 모든 개발의 흐름을 Angular 방식에 맞춰 개발하는 접근 방식과 다르게 자신 스스로가 원하는 애플리케이션 구조를 사용할 수 있다. 이 라이브러리는 인터페이스 레이어(interface layer)에 해당하기 때문에 풍성한 SPA 기능 대신에 각 페이지에서 사용할 수 있는 가벼운 기능을 제공한다. 이러한 접근 방식은 다른 라이브러리와 조합해서 사용하는데 넉넉한 공간을 제공한다. 물론 구조적 결정에 대한 책임도 생긴다. 예를 들면 Vue.js 코어에서는 기본적으로 라우팅이나 ajax 기능이 포함되지 않는다. 그리고 애플리케이션을 만드는 대부분의 경우에 외부 모듈 번들러를 사용한다고 가정하고 있다. 이런 특징을 가장 중요한 차이로 볼 수 있다.
    • Angular는 각 스코프(scope) 사이에서 양방향 바인딩(two-way binding)을 사용한다. Vue도 명시적 양방향 바인딩을 지원하긴 하지만, 기본 설정은 컴포넌트(component) 간, 부모에서 자식으로 단방향 바인딩(one-way)으로 구성되어 있다. 대형 앱에서는 단방향 바인딩을 사용하면 데이터의 흐름을 만들기 더 쉽기 때문이다.
    • Vue.js는 디렉티브와 컴포넌트를 명확하게 분리한다. 디렉티브는 DOM 조작을 캡슐화한 기능이고 컴포넌트는 뷰 자신과 데이터 로직을 포함한 독립 단위를 뜻한다. Angular에서는 이 둘의 차이가 상당히 혼란스럽다.
    • Vue.js는 변경 확인(dirty checking)을 수행하지 않기 때문에 좋은 성능을 제공하고 매우 매우 쉽게 최적화 할 수 있다. Angular는 감시자(watcher)가 늘어날 때마다 느려진다. 매번 스코프가 변경될 때마다 모든 감시자가 평가를 다시 수행하기 때문이다. 게다가 이 평가 흐름(digest cycle)에서 감시자가 다른 갱신을 수행하게 되면 모든 데이터가 “안정화(stabilize)” 될 때까지 반복하게 된다. Angular 사용자는 이런 상황을 해결하기 위해서 종종 난해한 기법을 사용하기도 하고 어떤 상황에서는 너무 많은 감시자가 있다보니 아예 간단하게 최적화 할 방법이 존재하지 않을 때도 있다. Vue.js를 사용한다면 이런 상황으로 고통 받을 필요가 없다. 비동기 큐 형태의 옵저버 시스템을 구현해서 의존성을 투명하게 관리하기 때문이다. 의존 관계가 명시적으로 기록되지 않았다면 모든 변경에 대해 독립적으로 이벤트를 호출한다. 앞으로 필요하게 될 최적화에 대한 힌트를 준다면 v-for 목록의 track-by 파라미터를 확인해보자.

    Angular 1의 문제를 해결하기 위해서 Angular2 와 Vue가 접근한 방식이 다소 비슷하다는 점은 흥미로운 사실이다.

    React

    React와 Vue.js는 반응형 & 조합 가능한 뷰 컴포넌트를 제공한다는 점에서 유사점을 공유한다. 물론 많은 차이점도 존재한다.

    먼저 내부 구현이 근본적으로 다르다. 리액트의 렌더링은 가상 DOM에 의해 이뤄진다. 가상 DOM은 메모리에서 실제 DOM이 어떤 형태로 존재하는지 저장하는 방식이다. 상태가 변경되면 React는 가상 DOM 전체를 다시 생성한 다음에 DOM을 비교하고 변경된 정보를 실제 DOM에 반영한다.

    가상 DOM 접근은 뷰가 어떤 상황에서든 값에 따라 동일하게 동작하는 함수적 접근 방식을 제공하고 있으며 정말 좋은 방법이라 할 수 있다. 전체 앱을 매 차례 다시 생성한다면 관찰자를 만들 필요도 없고 뷰는 항상 데이터와 동기화 되어 있다는 점을 명확하게 보증하기 때문이다. 게다가 이 접근 방식은 동형(isomorphic) 자바스크립트 애플리케이션에 대한 가능성도 열었다.

    Vue.js는 실제 DOM을 템플릿으로 사용하고 데이터를 실제 노드에 참조해서 사용하고 있다. 즉, Vue.js의 환경은 DOM에서 표현하는 방식으로만 사용 가능하다는 제약이 있다. React가 가상 DOM을 사용해서 다른 것에 비해 빠르다고 생각할 수 있지만 이 일반적인 오해와는 반대로 직접 갱신(hot update)에 있어서는 Vue.js가 React에 비해 손수 최적화하지 않고도 훨씬 빠르게 동작한다. React를 사용하는 경우에는 shouldComponentUpdate을 모든 위치에 구현해야 하며 불변 데이터 구조를 사용해야 다시 렌더링하게 되는 경우에 완벽한 최적화를 수행할 수 있다.

    API 단위에서 React(또는 JSX)의 문제는 함수를 렌더링하는데 종종 많은 로직이 동반되는 경우가 많고 결과적으로 인터페이스의 시각적인 표현보다는 그 자체로 작은 프로그램 조각이 되고 만다. 개발자 일부에게는 이런 특징이 이익으로 느껴지겠지만 나처럼 디자이너/개발자를 겸하는 사람에게는 템플릿을 만들어서 디자인과 CSS를 더 시각적으로 생각하는 방식이 훨씬 쉽게 느껴진다. JavaScript 로직이 섞인 JSX는 디자인에 코드를 적용하기 전까지 확인이 쉽지 않은 방식이다. Vue.js는 대조적으로 경량의 데이터 바인딩을 비용으로 지불하는 대신에 시각적으로 확인 가능한 템플릿을 제공하고 로직은 디렉티브와 필터를 사용해서 캡슐화 하는 방식을 사용한다.

    React의 다른 문제는 DOM 갱신이 전적으로 가상 DOM에서 이뤄지기 때문에 DOM을 직접 제어하고 싶은 경우에 다소 까다롭다. (이론적으로 가능하긴 하지만 라이브러리의 방식에 반해서 작업 해야한다.) 애플리케이션에서 DOM 조작에 부차적인 제어가 필요한 경우에는 이런 제한적인 특성으로 인해 짜증나는 작업이 되고 만다. 특히 요구사항이 시간적 흐름에 따라 변화하는 애니메이션이 그렇다. 반면 이런 문제에서 Vue.js는 더 유연하기 때문에 큰 문제가 되질 않는다. FWA/Awwwards에서 수상한 사이트 다수가 Vue.js로 만들어진 이유가 거기에 있다.

    아래는 몇 가지 살펴볼 만한 내용이다.

    • React 팀은 React를 모든 플랫폼 UI 개발에서 사용하려고 하는 야망을 갖고 있는 반면에 Vue는 웹을 위한 실용적인 해결책을 제공하는데 촛점을 두고 있다.
    • React는 함수적인 환경을 제공하고 있어서 함수형 프로그래밍 패턴과 아주 잘 맞는다. 이런 특징은 초보자나 주니어 개발자에게 큰 학습 장벽이 된다. Vue는 훨씬 간단하게 바로 사용할 수 있어서 더 효율적이다.
    • 대형 애플리케이션을 개발하는 경우에 React 커뮤니티에서는 상태 관리를 위한 해결책으로 Flux, Redux와 같은 혁신이 있었다. Vue 자체는 이런 문제를 크게 괘념치 않는데 (React 코어도 동일하다.) 비슷한 아키텍쳐라면 상태 관리 패턴은 쉽게 이식할 수 있는 개념이다. Vue 자체에서 사용할 수 있는 상태 관리 솔루션으로 Vuex가 있고, Redux를 Vue와 사용하는 것도 가능하다.
    • React 개발의 트랜드는 CSS까지 모든 것을 JavaScript에 집어넣는 분위기다. JS에 CSS를 넣는 많은 해결책이 존재하지만 각각 크고 작은 문제를 갖고 있다. 가장 중요한 문제는 표준 CSS 저작 경험을 벗어나고 있는데다 기존 CSS 커뮤니티가 일궈 놓은 작업을 이상하게 만드는데 지렛대 역할을 하고 있다. Vue의 단일 파일 컴포넌트는 컴포넌트 단위로 캡슐화된 CSS를 작성할 수 있고 선택에 따라서 전처리기를 선택하는 것도 가능하다.

    Ember

    Ember는 모든 기능을 제공하는 프레임워크로 아주 의견지향적으로 디자인되었다. 이 프레임워크는 많은 양의 컨벤션을 제공한다. 제공하는 모든 문법에 충분히 익숙해지면 아주 생산적으로 활용할 수 있다. 하지만 학습 곡선이 높은 데다 유연함이 고통을 준다. 의견지향적 프레임워크와 느슨한 의존성으로 묶인 여러 라이브러리 중 어느 것을 선택하느냐에 따라 얻거나 잃을 수 있는 부분이다. 후자를 선택하면 더 자유롭긴 하지만 구조적 결정을 내려야 하는 상황에 놓인다.

    Vue.js 코어와 Ember의 템플릿, 개체 모델 레이어를 비교하는 것이 더 나을 것이다.

    • Vue는 눈에 거슬리지 않는 반응성을 일반 JavaScript 개체를 통해 제공하며 모든 프로퍼티가 자동으로 연산된다. Ember에서는 모든 Ember 개체로 감싸야 하며 연산 프로퍼티를 사용하려면 수동으로 의존성을 선언해야 한다.
    • Vue의 템플릿 문법은 JavaScript 표현식 전체를 사용할 수 있는 반면에 Handlebar 표현식과 헬퍼 문법은 다소 제한적이다.
    • 성능에 있어서 Ember가 2.0에서 Glimmer 엔진으로 변경했는데도 여전히 Vue가 빠르다. Vue는 자동으로 일괄 갱신을 수행하는 반면 Ember는 성능에 민감한 상황에서 실행 순환을 수동으로 관리해야 할 필요가 있다.

    Polymer

    Polymer는 Google이 지원하는 또 다른 프로젝트다. 사실 Vue.js도 이 라이브러리에서 영감을 받고 만들었다. Vue.js의 컴포넌트는 Polymer의 커스텀 엘리먼트와 느슨하게 비교되는데 이 두 기능은 아주 비슷한 개발 스타일을 제공한다. 가장 큰 차이점은 Polymer가 최신 웹컴포넌트 기능 위에서 개발되었고 기능이 제공되지 않는 브라우저에서 사용하기 위해서는 폴리필이 필수적으로 필요하다. (폴리필이라서 성능도 떨어진다.) 이 특징과 대조적으로 Vue.js는 IE9까지 어떤 기술 의존 없이도 잘 동작한다.

    또한 polymer 1.0 팀은 성능을 챙기기 위해서 데이터 바인딩을 매우 제한적으로 가능하게 만들었다. 예를 들면, Polymer 템플릿의 표현식은 불린 부정(boolean negation)과 단일 메소드 호출만 지원한다. 또한 연산 프로퍼티의 구현이 아주 경직되어 있다.

    마지막으로 프로덕션에 배포할 때는 Polymer 엘리먼트를 Polymer에 특화된 도구인 vulcanizer를 사용해서 번들링 해야한다. 이와 대조적으로 Vue 컴포넌트는 단일 파일로 Webpack 생태계가 제공하는 모든 기능을 사용할 수 있다. 이 특징 덕분에 Vue 컴포넌트에 ES6도 쉽게 적용할 수 있고 CSS 전처리기도 필요하다면 바로 사용할 수 있다.

    Riot

    Riot 2.0은 유사한 컴포넌트 기반 개발 모델을 제공한다. (Riot에서는 “tag”라고 부른다.) 이 모델은 작고 아름답게 디자인된 API를 제공한다. 내 생각에는 Riot과 Vue는 디자인 철학을 많이 공유하고 있다. Vue는 Riot에 비해 조금 무겁지만 Riot에 비해 중요한 잇점을 제공한다.

    • 참 조건부 렌더링 (Riot 렌더링은 브랜치에 있다면 모든 내용을 렌더링하고 단순히 보여주고 숨기는 기능만 수행한다.)
    • 더 강력한 라우터 (Riot의 라우팅 API는 지나치게 단순하다.)
    • 더 성숙한 도구 지원 (webpack + vue-loader를 확인한다.)
    • 트렌지션 효과 시스템 지원 (Riot에는 없다.)
    • 더 나은 성능 (Riot은 사실 가상 DOM보다 변경 확인(dirty checking)을 사용하고 있어서 Angular와 동일한 성능 이슈가 발생한다.)

    최근에 화웨이 P9 Plus를 구입해서 오랜만에 안드로이드 환경을 사용하기 시작했다. 롬을 변경하거나 하지 않고 기본 EMUI를 사용하고 있는데 기본 한국어 폰트가 너무 안이뻐서 폰트를 변경하게 되었다. 혹시나 싶어 기록삼아 포스팅을 남기게 되었다.

    그냥 ttf 넣는다고 바로 변경되지 않아서 한참 검색했는데 이전 버전은 간단하게 가능한 반면 4 버전 이상에서는 쉽게 되질 않아 결국 편법으로 변경했다. 폰트를 변경하는 앱이 있나 찾아보니 있긴 한데 평이 전부 더이상 되지 않는다는 얘기라서 설치해보지 않았다.

    • 먼저 Themes > Fonts 에서 무료 폰트를 아무거나 받는다.
      • 이 과정에서 화웨이 ID가 필요한데 폰에서 가입이 진행이 안되길래 화웨이 웹페이지에서 가입했다. (찾아보면 가입 주소가 상당히 다양하다.)
    • 새 폰트를 받으면 HWThemes/HWFonts 디렉토리가 생기고 새 폰트가 ttf 파일로 들어있다.
    • 사용하려고 하는 폰트를 위에서 받은 폰트 이름과 동일하게 변경해서 넣는다. (otf나 ttc 파일의 경우에도 이름만 ttf로 설정해서 넣어도 문제 없다.)
    • Themes > Fonts 에서 해당 폰트로 설정한다.
      • 미리보기는 그냥 이미지라서 무료 폰트 모양이지만 폰트는 변경한 ttf로 적용된다.

    분명 메타데이터를 같이 넣는 방법이 있을 것 같은데 인터넷서 찾은 방법으로는 적용이 되질 않았다. 폰트와 xml 메타 데이터를 생성해서 <fontname>.hwt 폴더나 zip으로 압축해서 해당 이름으로 변경해서 넣으면 된다고 하는데 버전 차이인지 인식이 안된다. 서체는 Noto Sans Demi-Light가 가장 깔끔했다.

    이전까지는 lubuntu에 있던 xterm을 비트맵이 정겨워서 그냥 사용했는데 특수 기호를 표시하는데 불편함이 있어서 터미널을 변경하며 손 본 기록을 남긴다. 지금 사용하는 환경은 별 특별한 내용 없이 기본 lubuntu 설치 상태다.

    gnome-terminal 설치

    다른 터미널 다 살펴봤는데 우분투 기본 터미널인 gnome-terminal 쓰기로 했다. lubuntu에서 제공하는 lxterminal은 별로 안이쁘다.

    $ sudo apt-get install gnome-terminal
    

    시작할 때 터미널 기본 시작

    전체화면을 위한 플래그 셋, 그리고 시작 후에 터미널에서 tmux를 실행하도록 작성했다. 동일한 내용으로 /usr/local/binterminal 스크립트를 추가했다.

    #!/bin/bash
    gnome-terminal --window --hide-menubar --full-screen -e tmux
    

    시작 -> 기본 설정 -> Default Applications for LXSession 들어가서 autostart에 terminal을 추가했다.

    기본 IM와 연동되도록 플러그인 설치 fcitx.vim

    Lubuntu에는 Fcitx-IM가 기본 IME다. Vim에서 모드를 전환하는 경우에 영문 자판이 아니면 매번 한영전환을 해야 하는 번거로움이 있는데 이 fcitx.vim 플러그인이 그 해결책이다. 이 플러그인은 Vim에서 모드를 전환하면 영문으로 전환하고 다시 끼워 넣기 모드로 돌아올 때, 이전 언어로 자판을 변경한다.

    fcitx.vim 받아서 ~/.vim/plugin에 해제하면 끝난다.


    그 외 터미널 시작 시에 메시지를 넣었는데 별 내용 없이 .zshrcecho로 넣었다. 분실 연락처 정도 넣었다. 아직 Vim을 사용해야 해서 아직 neovim으로 넘어가지 않았는데 얼른 하는 일 마무리하고 넘어가고 싶다. 이상하게 nvm이 터미널 시작을 느리게 만들어서 일단 주석 처리 해뒀다. 조만간 다시 확인해봐야겠다.

    오랜만에 깔끔한 폰트로 터미널을 사용하니 또 새롭다. 바쁜 일을 좀 정리한 후에는 환경을 좀 더 꾸며보고 싶다.

    색상을 바꿔요

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

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