ECMAScript use strict 사용하기

“use strict” 은 자바스크립트 코드를 좀 더 엄격한(strict) 모드에서 실행하도록 인터프리터에게 명령하는 자바스크립트의 지시자(directive)다.

좋은 점 3가지

  1. 코딩 실수 대신 에러를 낸다. 따라서 프로그래머는 자신의 실수를 쉽게 알아차릴 수 있다.
  2. 보통 엄격 모드에서 실행속도가 빠르다라고 알려져 있다. 직접 테스트를 안해봐서 알 수는 없지만, 엄격 모드에서는 인터프리터의 최적화 작업을 어렵게 만드는 문법을 금지시키기 때문에 빠르다.
  3. ECMAScript의 차기 버전들에서 정의 될 문법을 사용하지 못한다.

‘엄격 모드’를 사용하면 생기는 제한

이 ‘엄격 모드’로 자바스크립트 코드를 실행하고자 하면 은은하게 많은 제한이 생긴다. 주석은 크롬 콘솔에서 실행했을 때 발생하는 에러다.

  1. 생성하지 않은 변수를 사용할 수 없다. 자바스크립트에서는 실수로 변수명을 잘못 적었다면 에러를 내지 않고 그냥 새로운 전역 변수가 생성되는데, strict 모드를 사용하면 이를 막을 수 있다.

    1
    2
    "use strict";
    pi = 3.1415; // Uncaught ReferenceError: pi is not defined
  2. 변수나 함수, 삭제할 수 없는 속성을 삭제할 수 없다.

    1
    2
    3
    4
    5
    6
    "use strict";
    var pi = 3.14;
    delete pi;
    function f(p1, p2) {};
    delete f; // Uncaught SyntaxError: Delete of an unqualified identifier in strict mode.
    delete Object.prototype; // TypeError: property "prototype" is non-configurable and can't be deleted
  3. 파라미터 이름을 중복으로 쓸 수 없다.

    1
    2
    "use strict";
    function f(p1, p1) {}; // SyntaxError: duplicate formal argument p1
  4. 8진수 리터럴, 8진수 이스케이프 문자를 사용할 수 없다.

    1
    2
    3
    "use strict";
    var oct = 010; // Uncaught SyntaxError: Octal literals are not allowed in strict mode
    var oct1 = "\010"; // Uncaught SyntaxError: Octal escape sequences are not allowed in strict mode.
  5. 읽기 전용 속성이나 GET 전용 속성을 다시 쓸 수 없다.

    1
    2
    3
    4
    5
    6
    "use strict";
    var obj = {get z() {return 0} };
    Object.defineProperty(obj, "x1", {value:123, writable:false});

    obj.x1 = "hello"; // Uncaught TypeError: Cannot assign to read only property 'x1' of object
    obj.z = "world"; // Uncaught TypeError: Cannot set property z of #<Object> which has only a getter
  6. 몇 가지 키워드를 변수명으로 사용할 수 없다.

    1
    2
    3
    "use strict";
    var eval = 'hello'; // SyntaxError: 'eval' can't be defined or assigned to in strict mode code
    var arguments = 'world'; // SyntaxError: 'arguments' can't be defined or assigned to in strict mode code

브라우저 지원

2009년 ECMAScript 5에서 처음 나온 기능으로 IE9과 그 이전 IE에서는 strict 모드를 지원하지 않는다. 다만 이전 브라우져에서 큰 문제는 되지 않는데, 이전 브라우져에서는 문법적(BNF)으로 보면 “use strict”를 그저 리터럴 표현으로 인식하기 때문에 side effect 를 발생시키지 않고 따라서 그냥 무시될 뿐이다.

strict 모드를 지원하는 브라우져는 아래와 같다.

  • 크롬 버전 10 이상
  • 파이어폭스 버전 4 이상
  • 사파리 버전 5.1 이상
  • IE 버전 10 이상
공유하기 댓글

ECMAScript(Javascript) 호이스팅

호이스팅(Hosting)은 자바스크립트 인터프리터가 변수 및 함수의 선언을 항상 코드 상단으로 올리는 행위를 말한다. 엄밀하게 얘기하자면 컴파일 단계에서 변수 및 함수의 선언을 먼저 읽어서 메모리 상에 저장해두는 행위에 가깝다. 예제 코드를 한 번 보자.

1
2
3
a = 1;
console.log('Result:', a);
var a;

이 코드의 실행 결과는 아래와 같다.

1
Result: 1

알다시피 자바스크립트는 변수를 선언하기 전에 사용할 수 있다. 코드 1행에서 선언하지도 않았던 a에 1을 할당했고 2행에서 콘솔에 출력한 결과가 제대로 출력되었다. 정작 변수 a는 3행에서야 나타난다.

이를 호이스팅(hoisting)이라 한다. 호이스팅의 원형 hoist는 사전적 의미로 밧줄이나 장비를 이용하여 끌어올리다는 뜻이 있다. 이렇게 자바스크립트 인터프리터는 컴파일 단계에서 변수 및 함수의 선언을 항상 컨텍스트의 상단으로 끌려올린다. 인터프리터가 선언 코드를 물리적으로 상단으로 올리는 건 아니다. 다만 변수와 함수 선언을 컴파일 단계에서 메모리에 올려두고 undefined 값을 할당한다. 그 후에 코드를 실행하기 때문에 선언이 뒤에 있더라도 선언 앞에서 해당 변수를 사용할 수 있다.

다만 선언에 강조를 한 이유는 초기화 코드는 호이스팅의 대상이 아니기 때문이다.

1
2
3
var a = 5;
console.log('a: %d b: %d', a, b);
var b = 7;

위 코드를 크롬 콘솔에서 실행하면 아래의 결과가 나온다.

1
a: 5 b: NaN

자바스크립트 인터프리터가 위 코드를 호이스팅하면 아래와 같은 형태로 코드를 읽어들인다.

1
2
3
4
5
var a;
var b;
a = 5;
console.log('a: %d b: %d', a, b)
b = 7;

즉 인터프리터는 컴파일 단계에서 변수 a와 b를 메모리 상에 올려두고 undefined 값을 할당해 둔다. 다만 호이스팅은 선언만을 대상으로 하기 때문에 초기화 구문은 해당되지 않는다. 따라서 console로 값을 찍을 무렵에는 a에는 5만 할당되어 있고 b는 undefined 상태가 된다.

함수도 호이스팅의 영향을 받는다. 함수 선언식이 호이스팅의 영향을 받는다. 자바스크립트에서 함수를 사용하는 방식을 잠깐 설명하자면 자바스크립트에서 함수를 사용하는 방식은 함수 선언식(Function Declaration)과 함수 표현식(Function Expression)으로 구분할 수 있다.

함수 선언식은 다른 프로그래밍 언어에서 사용하는 함수 선언식과 동일하다.

1
2
3
function functionDeclaration() {
// 함수 내용
}

함수 표현식은 아래와 같다. Go언어같은 최신 언어에서는 자바스크립트와 같은 함수 표현식을 많이 지원하기도 한다.

1
2
3
var functionExpr = function() {
// 함수 내용
}

예를 들어 아래와 같은 코드가 있다면,

1
2
3
4
5
6
7
8
9
10
funcDecl();
funcExpr();

function funcDecl() {
return 'decl!';
}

var funcExpr = function() {
return 'expr!';
}

호이스팅때문에 인터프리터는 아래와 같이 해석한다.

1
2
3
4
5
6
7
8
9
10
function funcDecl() {
return 'decl!';
}
var funcExpr;
funcDecl();
funcExpr();

funcExpr = function() {
return 'expr!';
}

따라서 funcExpr()를 실행할 시점에는 funcExpr()이 정의되지 않았기 때문에 크롬에서 실행하면 아래와 같은 결과가 나온다.

1
Uncaught TypeError: funcExpr is not a function

이 호이스팅 때문에 의도하지 않은 버그를 내기도 한다. 문제는 자바스크립트 인터프리터의 동작을 이해하지 못하는 사람이 코드를 짜면 버그가 잘 날 수 있다는 사실, 그리고 그 버그가 나오면 찾기 어렵다는 문제가 있다. ~C계열의 undefined behavior의 향기가…
따라서 호이스팅을 막으려면

  1. 선언을 함수나 전역 상단에 적기
  2. let 사용
  3. strict 모드 사용 - use strict 을 사용 시 선언하지 않은 변수를 사용할 수 없다.
공유하기 댓글

구글 앱 엔진 소개

구글 앱 엔진(Google App Engine)은 구글 클라우드 플랫폼에서 구동되는 웹 프레임워크입니다. HTTP/HTTPS 요청을 처리하는 서비스로서 아마존 웹서비스(이하 AWS)의 Beanstalk와 유사합니다. 하지만 이 둘 사이에는 결정적인 차이가 있습니다. 구글 앱 엔진은 완전 관리형(Fully managed) 서비스, AWS는 일부 관리형 서비스라는 점이죠.

쓸만한가요?

근무하고 있는 곳에서는 REST API를 구글 앱 엔진에 올려서 사용하고 있으며, 하루에 수 백 ~ 수 천만건의 요청을 처리하고 있는 중입니다. 일하는 기간 동안에 구글 앱 엔진에서 문제가 발생한 적은 없었습니다. 장애는 주로 DB쪽이나 다른 모듈에서 문제가 발생해서 생긴 적이 대부분이었습니다.

구글 앱 엔진의 장점

쉽고 간단하다

같은 서비스를 EC2를 사용해 구축한다고 하면 Elastic Load Balancer + Auto Scaling Group + EC2 + Route53 설정 등을 해줘야하고 추가적으로 배포도 신경 써줘야 하며, 앱이 죽었는지 살았는지 Health Check도 필요합니다. 하지만 구글 앱 엔진은 이런게 필요없습니다. Load Balancer와 Scaling 설정은 스스로 혹은 간단한 옵션으로 관리하며 인스턴스가 죽으면 자동으로 해당 인스턴스를 자동으로 재시작합니다.

무중단 서비스는 기본이다

내부적으로 Blue Green Deployment를 구현, 새로운 앱을 배포하면 앱 엔진 내부에서 새로운 앱을 배포 후 라우팅을 새로운 앱으로 돌립니다.

강력한 HTTP URL기반 라우팅

개인적으로 매우 마음에 드는 기능입니다. 태그에 의존하는 AWS과는 달리 구글 클라우드는 프로젝트 이름 단위로 리소스가 확실히 구분됩니다. 구글 클라우드에서 example 이라는 프로젝트를 만들었으면 앱 엔진의 URL주소는 https://example.appspot.com이 됩니다. 배포용은 가만히 냅두고 개발용으로 별도의 인스턴스를 만들어서 사용하고 싶을 경우에는 개발용 버전(예: staging)을 배포하면서 이 인스턴스에 트래픽을 주지말라는 옵션을 줍니다. 그리고 개발용 인스턴스에 요청을 보내고 응답을 받고 싶을때는 그냥 https://staging-dot-example.appspot.com 이라고 호출하면 자동으로 개발용 버전으로 해당 트래픽을 연결합니다.

안쓰면 0원 & 지속적으로 사용하면 최대 30% 할인

별도 옵션을 두지 않을 경우 일정 시간동안 요청이 없으면 Instance 갯수가 0이 됩니다. 즉 과금이 없습니다. 펫 프로젝트 할 때 제격이죠. 또한 지속적으로 인스턴스를 켜두면 알아서 30% 정도 깎아줍니다. AWS처럼 스팟 인스턴스니 뭐니 하면서 요금에 신경을 조금 덜 써도 됩니다.

구글 앱 엔진의 단점

한국 리전이 없다

AWS와는 달리 구글 앱 엔진은 한국 리전이 없습니다. 당분간 제일 가까운 리전은 도쿄 리전인데 보통 30 ~ 50ms 정도 Latency를 보입니다. 보통 AWS 서울 리전은 10 ~ 20ms 의 Latency를 보이는 것에 비해서 약간 느린 편이죠.

언어 및 언어 버전에 제약이 있다

지원하는 언어는 Python, Java, PHP, Go 뿐입니다. 아니 구글 앱 엔진 문서에는 C#, Node.js 도 적혀있는데 왜 이러시냐고 묻으실 분들은 아래 조금만 더 읽어주시면 됩니다 : )

프레임워크와 궁합이 좋지 않다

구글 앱 엔진은 완전 관리형 시스템인 PaaS 서비스다보니까, 제약이 상당히 많습니다. Go 언어의 예를 들죠. 일반적인 Go 어플리케이션의 Main은 아래와 같은 형태를 띕니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", handle)
log.Fatal(http.ListenAndServe(":8080", nil))
}

func handle(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
fmt.Fprint(w, "Hello world!")
}

하지만 구글 앱 엔진의 Main의 형태는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"net/http"

"google.golang.org/appengine"
)

func main() {
http.HandleFunc("/", handle)
appengine.Main()
}

func handle(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world!")
}

뭐가 다른지 아시겠나요? 웹 어플리케이션은 라우팅 설정을 한 후 특정 포트를 Listen 상태로 들어가는데 앱 엔진에는 이 부분이 없습니다. 따라서 모든 부분을 관리하는 모노리스 웹 프레임워크 중 일부는 웹 엔진에서 제대로 구동이 안될 수 있습니다. 장고(Django) 도 초반에는 앱 엔진에서 제대로 돌릴 수 없었고 Go 언어의 Revel 역시 앱 엔진에서 돌릴 수 없습니다.

Flexible Environment 의 최소 비용이 비싸다

여기서 앞에서 언급한 단점인 ‘언어에 제약이 있다’에 대해서 설명할 수 있습니다. 구글 앱 엔진에서는 Standard Environment(이하 SE)와 Flexible Environment(이하 FE)가 있습니다. 이 둘의 결정적인 차이는 SE는 샌드박스 환경에서 실행되고 FE는 AWS의 EC2와 같은 가상 컴퓨팅 머신 위에서 실행된다는 차이입니다. 따라서 FE는 웹 프레임워크의 제약이 없고 언어의 제약도 많이 없지만 SE는 샌드박스 상에서 돌아갈 수 있는 런타임 위에서만 구동됩니다. 예를 들어 Python 3.x가 십 수년전부터 존재함에도 불구하고 SE상에서는 Python 2.7만 지원하며, PHP7이 나왔음에도 SE에서는 PHP5.5만 지원하지요.

SE는 인스턴스 시간당 비용으로 과금되지만 FE는 CPU, Memory, 디스크 용량의 사용량으로 과금한다는 점이 다릅니다. 암튼 EC2와 같은 가상 컴퓨팅 환경에서 돌아가는게 FE라고 했는데 가장 싼 CPU 타입으로 해도 월 $40을 피할 수는 없습니다. 이는 EC2의 가장 저렴한 인스턴스 타입인 t2.micro가 $8.5에 비한다면 매우 비쌉니다. 개인적으로 이 부분이 가장 아쉬운 부분입니다.

SE와 FE의 차이에 대한 글 : https://cloud.google.com/appengine/docs/the-appengine-environments

공유하기 댓글

Jest로 Express 테스트하기

Jest로 Express API 유닛 테스트를 할 때 겪었던 문제를 다른 사람들도 안 겪었으면 하는 바램에서 글을 썼다. 원래 유닛 테스트는 MochaChai를 사용했는데 이번에 한 번 Jest로 테스트 프레임워크를 바꿔보았다.

Jest를 사용하기 위해서는 일단 Install.

1
npm install --save-dev babel-cli, babel-preset-env, jest, supertest

아래는 간단한 웹 서버 코드다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app.js
import express from 'express';

const app = express();
const port = 3000;

app.get('/hello', (req, res) => {
res.send('world!');
});

app.get('/status', (req, res) => {
res.send('ok');
});

app.listen(port);

module.exports = app;

매우 간단한 이 웹 서버는 두 개의 라우트(/hello, /status)밖에 없다. /hello 라우트는 단순하게 “world!” 문자열만 리턴하며 /status는 단순하게 “ok” 문자열만 리턴한다.

이제 Jest와 Supertest를 사용해서 이 라우트를 테스트를 해보겠다.

먼저 /hello

1
2
3
4
5
6
7
8
9
10
11
12
// tests/hello.test.js
import request from 'supertest';
import app from '../app';

describe('Test /hello', () => {
it ('should return world!', (done) => {
request(app).get('/hello').then((response) => {
expect(response.text).toBe('world!');
done();
});
});
});

Test!

깔끔하게 성공한 것을 볼 수 있다.

그럼 이제 /status에 대한 테스트를 해보자. 이 테스트는 다른 파일에 작성하도록 한다. 물론 식은 죽 먹기다.

1
2
3
4
5
6
7
8
9
10
11
12
// tests/status.test.js
import request from 'supertest';
import app from '../app';

describe('Test /status', () => {
it ('should return ok', (done) => {
request(app).get('/status').then((response) => {
expect(response.text).toBe('ok');
done();
});
});
});

Test! fire

scream 당연히 성공할 줄 알았던 테스트가 EADDRINUSE 에러를 내면서 실패했다. 문제는 첫 번째 테스트를 실행하면서 서버는 아직도 3000번 포트를 listen 하고 있기 때문이다. 그래서 두 번째 테스트에서 다시 import app from ‘../app’ 을 실행하면 이전 테스트에서 이미 listen하고 있는 3000번 포트를 다시 사용하기 때문에 문제가 발생한 것이었다.

이 문제를 해결하기 위해서는 테스트가 아닐 경우에만 3000번 포트를 열도록 변경해야 한다. 어차피 테스트 환경일 경우에는 Supertest를 통해서 서버를 구동하기 때문에 네트워크 포트를 열 필요가 없다. 따라서 package.json을 아래와 같이 바꾼다.

1
2
3
4
5
{
"scripts": {
"unit": "NODE_ENV=test jest tests/*.test.js --forceExit",
}
}

그리고 app.js를 아래와 같이 바꾼다.

1
2
3
4
// ...
if (process.env.NODE_ENV !== 'test') {
app.listen(port);
}

다시 한 번 실행하면

성공!

공유하기 댓글

go 언어 구글 클라우드 Datastore 패키지를 잘 씁시다!

3월 초에 시간이 좀 많이 나서 펫 프로젝트를 진행한 일이 있다. 구글 클라우드에서 AppEngine Standard Environment + Datastore으로 프로젝트를 구성했다. AppEngine은 복습할 겸 회사에서 지겹도록 파이썬을 써서 Go언어로 만들었다. API는 쉽게 구현할 수 있었다. 다만 문제는 속도가 너무 느렸다. 아래는 대략적인 코드다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package mypetproject

import (
"context"
"net/http"

"github.com/gorilla/mux"
"cloud.google.com/go/datastore"
"google.golang.org/appengine"
)

func handleTable(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)

dsClient, err := datastore.NewClient(ctx, "my-project")
if err != nil {
// Handle error.
}
k := datastore.NameKey("Entity", "stringID", nil)
e := new(Entity)
if err := dsClient.Get(ctx, k, e); err != nil {
// Handle error.
}
// ...
}

func init() {
r := mux.NewRouter()
r.HandleFunc("/table", handleTable).Methods("GET")

http.Handle("/", r)
}

하나의 Kind(관계형DB의 Table)에 몇 개 되지도 않은 Entity(관계형DB의 Row)만 있고 단순한 GET 만을 했을 뿐인데 속도가 600~800ms 나 걸렸다! 제일 의심이 가던 부분은 datastore.NewClient()였다. 이 함수에서 Datastore에 연결하는데 오래 걸리리라 생각했다. 그래서 이를 패키지 전역으로 빼려고 했으나 실패. datastore.NewClient()의 인자로 context를 받는데, 이 context는 appengine의 context 를 받아야 했다.

1
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error)

구글이 이렇게 허접하게 만들었을리 없는데, 뭔가 다른 방법이 있지 않을까 하고 낑낑대던 와중에 눈에 띈 패키지명이 있었다. 바로 google.golang.org/appengine/datastore … 내가 사용했던 Datastore 패키지와 달랐다.

정리하자면

google.golang.org/appengine/datastore는 Google App Engine Standard Environment에서 사용하는 패키지다. godoc에서 확인해보면 맨 첫 머리에 아래와 같은 설명이 있다.

Package datastore provides a client for App Engine’s datastore service.

그렇다. google.golang.org/appengine/datastore 패키지는 App Engine Standard Environment 전용이며, 그 외에 로컬이나 Compute Engine, App Engine Flexible Environment에서 사용할 때는 cloud.google.com/go/datastore 를 사용해야 한다.

App Engine에서 부를때는 Client를 만드는 부분, 즉 인증, 연결 부분이 생략되어 있기 때문에 코드가 다음과 같이 조금 더 간단해졌다. 또한 응답시간 역시 기존 600~800ms에서 모두 80ms 아래로 떨어뜨릴 수 있었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package mypetproject

import (
"context"
"net/http"

"github.com/gorilla/mux"
"google.golang.org/appengine"
"google.golang.org/appengine/datastore"
)

func handleTable(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)

k := datastore.NewKey(ctx, "Entity", "stringID", 0, nil)
e := new(Entity)
if err := datastore.Get(ctx, k, e); err != nil {
http.Error(w, err.Error(), 500)
return
}
// ...
}

func init() {
r := mux.NewRouter()
r.HandleFunc("/table", handleTable).Methods("GET")

http.Handle("/", r)
}
공유하기 댓글