Programming language/Typescript

TypeScript class를 JavaScript ES5, ES6 code로 변환하면서

iKay 2020. 9. 3. 01:12
반응형

서론

TypeScript code는 실행되기 위해 JavaScript code로 필수적으로 변환하는 과정(transpile)이 필요하다. TypeScript는 NodeJS에서 실행할 수 있는 언어가 아니라 JavaScript에 정적 type을 사용할 수 있도록 한 언어이기 때문이다.

 

이러한 이유로 개인적으로 TypeScript code가 JavaScript code로 어떻게 변환되는지 어느 정도는 이해하는 것이 필요하다고 생각한다. 나는 주로 TypeScript로 백엔드 개발을 하기 때문에 NodeJS를 실행시키는데 JavaScript의 버전 호환성에 크게 민감하지 않지만, 프론트엔드 개발을 한다면 이 부분은 매우 중요할 것이다. 그래서 가끔은 TypeScript Code가 JavaScript Code로 어떻게 변환되는지 살펴보는 편이다.

 

흔하게 사용하는 TypeScript code class를 class 문법을 지원하지 않던 시절(ES5)의 JavaScript code로 변환하게 되면 어떤 결과가 나오는지 궁금했고, 이런 호기심 때문에 이번 글도 쓰게 된 것 같다.

 

 

JavaScript ES5 code 로 변환

다음은 User class를 Javascript ES5 code로 변환한 결과이다.

 

변환 전

class User {
    public id: number;
    public name: string;
    public language: string;
    private password: string;

    static from(
        id: number,
        name: string, 
        language: string,
        password: string,
    ) {
        return new User(id, name, language, password);
    }

    private constructor(
        id: number,
        name: string, 
        language: string,
        password: string,
    ) {
        this.id = id;
        this.name = name;
        this.language = language;
        this.password = password;
    }

    public checkPassword(password: string) {
        if (this.password !== password + '@') { 
            throw new Error('password가 일치하지 않습니다.');
        }
    }
}

 

변환 후

var User = /** @class */ (function () {
    function User(id, name, language, password) {
        this.id = id;
        this.name = name;
        this.language = language;
        this.password = password;
    }
    User.from = function (id, name, language, password) {
        return new User(id, name, language, password);
    };
    User.prototype.checkPassword = function (password) {
        if (this.password !== password + '@') {
            throw new Error('password가 일치하지 않습니다.');
        }
    };
    return User;
}());

 

여기서 의문점이 몇 가지 생겼다.

 

0. 왜 class가 이렇게 변환될까?

1. JavaScript는 왜 private field가 근본적으로 없는가?

2. static 메소드와 일바 메소드는 왜 다르게 변환되는가? 

 

이 질문에 대해 자문자답 해봤다.

 

자문자답

0. class 변환

궁금했던 점은 class를 변환할 때 왜 [1]즉시 실행 함수 표현식(Immediately Invoked Function Expression)을 사용 했을까 였다. 스스로 내린 결론은

 

a. JavaScript는 기본적으로 global 하게 동작한다. 이런 점 때문에 User class의 field 들을 한 scope에 제한하여 최대한 외부의 영향을 받지 않고, 외부에 영향을 끼치지 않게 하기 위해서이다.

 

b. 소스에는 반영되지 않았지만 private 한 멤버를 만들기 위해, 즉시 실행 함수 내부에 어떤 변수가 선언되어 있다면 내부 함수에서는 그 변수에 접근가능하겠지만 외부에서 객체가 생성된 후에는 직접 접근이 되지 않는다. 즉, 클로저를 이용해 private 한 멤버를 구현해내기 위해서 인것 으로 예상한다. 하지만, 실제 typescript에서 class의 멤버로 private 를 만들면 js에서는 접근 가능하다.

 

1. private field

결론부터 말하자면, JavaScript에는 private field가 없다. 위에는 없지만 protected field도 없다. 단지 편의상 TypeScript에서 제공할 뿐이다. 실제로 TypeScript에서도 User를 인스턴스화 하고 @ts-ignore로 주석처리해 TypeScript 에러를 무시하면, private field인 password에 접근이 가능하다.

 

이 점으로 보아 privte field에 접근하려하면 TypeScript는 단지 Syntax에러로 표시할 뿐이지 근본적으로 runtime에는 에러는 아니라는 결론을 내릴 수 있다.

 

function main() {
    const user1 = User.from(1, 'Alpha', 'ko', 'qqqq1111');
    
    // @ts-ignore
    console.log(user1.password); // qqqq1111
}

main();

 

하지만 최근 JavaScript에서도 [2]근본적인 private field를 지원하려는 움직임이 보이고 있고 몇 년 내에 정식 버전으로 채택될 가능성이 높아 보인다. TypeScript에서도 3.8 버전 이상이면 사용가능한 문법이라고 [3]릴리즈 노트에 소개하고 있다.

 

 

아래 코드는 위와 다르게 TypeScript code를 보면 privte field에 "#"이 붙은 것을 볼 수 있는데 이것이 private field를 선언하는 syntax이다.  ES5로는 변환이 되지 않아 ES6로 변환했다. 아마 WeakMap[4] 때문인 것으로 생각된다. [5]WeakMap이 ES6+ 이후에 사용 가능한 것 같다.

 

처음 질문이 TypeScript private field가 JavaScript code로 어떻게 변환될까 였지만, ESNext JavaScript의 private field가 ES6+ JavaScript code로 어떻게 변환되는가로 질문이 바뀐 것 같다. 하지만 이것도 한 번 살펴 보는 것도 가치가 있다고 생각한다. 보다시피 특징은 private field를 ES6+에서 사용하기 위해서는 WeakMap을 사용한다는 점인 것 같다.

 

WeakMap은 Map과 비슷하게 key, value 형식으로 저장할 수 있는 Collection이다. 차이점은 key에 object만 저장가능하고, key에 대한 참조가 더 이상 없는 경우 GC에 의해 바로 해제된다고 한다. 그래서 Map과 달리 key를 iterate할 수 없다. 여기서 왜 굳이 WeakMap을 사용했을까 고민했는데, key가 유효한 동안만 즉, key가 참조되는 동안만 즉, 객체가 참조되는 동안만 value(private field)를 참조하기 위해서가 아닐까 스스로 결론을 내렸다.

변환 전

class User {
    public id: number;
    public name: string;
    public language: string;
    #password: string;

    static from(
        id: number,
        name: string, 
        language: string,
        password: string,
    ) {
        return new User(id, name, language, password);
    }

    private constructor(
        id: number,
        name: string, 
        language: string,
        password: string,
    ) {
        this.id = id;
        this.name = name;
        this.language = language;
        this.#password = password;
    }

    public checkPassword(password: string) {
        if (this.#password !== password + '@') { 
            throw new Error('password가 일치하지 않습니다.');
        }
    }
}

변환 후

var _password;
class User {
    constructor(id, name, language, password) {
        _password.set(this, void 0);
        this.id = id;
        this.name = name;
        this.language = language;
        __classPrivateFieldSet(this, _password, password);
    }
    static from(id, name, language, password) {
        return new User(id, name, language, password);
    }
    checkPassword(password) {
        if (__classPrivateFieldGet(this, _password) !== password + '@') {
            throw new Error('password가 일치하지 않습니다.');
        }
    }
}
_password = new WeakMap();

 

2. static method vs 일반 method

static method는 prototype에 체이닝 되지 않고 직접 class에 체이닝 되어 선언되었다. 반면 일반 method는 prototype에 체이닝 되어 선언되었다. 큰 차이점은 prototype으로 보인다. 그렇다면 [6]prototype이 무엇인지를 살펴 보면 될 것 같다.

 

* 다음 단락의 내용은 정확히 이해하지 못 한 것 같아 보완이 필요해 보인다.

 

JavaScript는 객체 지향 언어이긴 하지만 보통의 다른 언어 객체 지향 언어와 달리 class 기반 언어가 아니라 prototype 기반 언어이다. class를 ES6+부터 사용할 수 있지만 보기에만 그럴 뿐 동작은 prototype 기반으로 한다고 한다. prototype은 모든 객체와 연결된다. 반면 prototype이 없다면, 그 대상에 직접 연결된다.

 

그러니깐 Java같은 경우 static 메소드는 static 영역의 메모리를 사용(class는 컴파일 시점 평가되니깐)하고 일반 객체의 메소드는 heap에 할당된 후 사용가능 하니깐 아예 다른 메모리 영역을 갖게 되는 차이가 있는 것으로 알고 있는데 이것도 정확하지 않아서 정리하면서 JavaScript와 비교해보면 될 것 같다.

 

결론

TypeScript class code를 ES5, ES6+ JavaScript code로 변환하면서 생각보다 JavaScript에 대해 알아야 하는 것이 많다는 것을 깨달았다. 덕분에 즉시 실행 함수 표현식의 목적,  JavaScript의 private field, WeakMap 그리고 prototype에 대해 정리하는 계기가 된 것 같다.

 

TypeScript Playgound(https://www.typescriptlang.org/play)에서 누구나 언제든지 TypeScript code를 JavaScript ES5, ES6+ code로 쉽게 변환시킬 수 있다. 다른 또 궁금한 점이 생길 때 다시 돌아와서 JavaScript를 고민해보면 될 것 같다.

 

참고

[1] 즉시 실행 함수에 대한 설명은 여기를 더 참고. https://developer.mozilla.org/en-US/docs/Glossary/IIFE 

[2] JavaScript private field에 대한 tc39 proposal, stage 3인 proposal로 몇 년 내에 정신 버전에 채택될 가능성이 높다고 개인적으로 생각한다. 현재는 ESNext 버전의 EcmaScript(JavaScript)에서 정식적으로 사용가능하다. https://github.com/tc39/proposal-class-fields/

[3] TypeScript 릴리즈 노트, private field에 대한 설명을 하고 있다. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#ecmascript-private-fields

[4] WeakMap. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap

[5] Map, Set, WeakMap 그리고 WeakSet은 ES6에 나온 collection이라고 소개하고 있다. https://www.sitepoint.com/es6-collections-map-set-weakmap-weakset/

[6] prototype에 대해 이 stackoverflow 설명을 참고했다. https://stackoverflow.com/questions/43474967/call-es5-class-method-from-static-method

 

 

 

 

반응형