JavaScript, TypeScript 에서 진짜 Private 구현하기
제목 : 자바스크립트에서 진짜 private 구현하기
현대의 자바스크립트 문법(es6 이상) 에서는 "class" 문법이 지원된다.
그러나, 자바스크립트를 배우는 지난 과정 속, 데코레이터(Decorator) 를 배우면서,
실질적으로 JavaScript 의 Class 또한, function 의 일환이라는 것을 발견했다.
이를 발견하게 된 것은, 기존의 TypeScript Class 코드를 JavaScript ES5 버전으로 변경하면서 발견했다.
Example :
typescript
// 클래스 컴파일 결과 확인용
class FunctionClass {
title : string;
static staticalMember : number = 100;
constructor() {
this.title = "클래스가 어떻게 함수가 되는거지?"
}
}
javascript
-- tsc
// 클래스 컴파일 결과 확인용
var FunctionClass = /** @class */ (function () {
function FunctionClass() {
this.title = "클래스가 어떻게 함수가 되는거지?";
}
FunctionClass.staticalMember = 100;
return FunctionClass;
}());
클래스와 프로토타입으로 생성된 인스턴스와,
클래스, 프로토타입, 인스턴스가 연결된 프로토타입 체인을 공부하며 헷갈렸던 연결 그 자체를,
컴파일 이후의 자바스크립트를 보면서 지식들을 찬찬히 연결시켜 나갔다.
클래스를 선언하는 방식이 언어 자체에 종속되어 있는 것이 아니라,
또 다른 함수를 변수에 담음으로서 클래스를 생성한다니, 정말 예상치도 못한 부분이였다.
var FunctionClass
는 결과적으로 내부의function FunctionClass() {...}
를 의미한다.var FunctionClass
에 클래스를 의미하는 함수 객체를 할당하는 로직을 작성하기 위해,
function FuntionClass()
메서드에 속성을 주입하고 반환하는 로직을 익명 함수, 즉시 실행 으로 작성했다.- 예를 들어
const instance = new FunctionClass()
에서instance.staticalMember
로
클래스의 정적 부분을 바로 접근할 수 없던데, 그것은 "별개의 함수" 였기 때문이었다. - 별개의 함수이지만, 결국 인스턴스의 클래스, 프로토타입의 연결 정보는
- prototype :
Object.getPrototype(instance)
- class :
Object.getPrototype(instance).constructor
로 구할 수 있었다. - 클래스와 프로토타입 객체는 서로의 연결 정보를 가지고 있기 때문에 위의 방식이 가능하다.
- prototype :
타입스크립트를 다시 번역한 자바스크립트의 문법을 보면서, private
적용 여부도 궁금했다.
자바스크립트 문법에도, 타입스크립트 문법에도 private 은 존재한다.
그렇다면, "클래스" 가 함수로 번역되는 자바스크립트에서 private
은 어떻게 나타날 것인가?
typescript
// 클래스 컴파일 결과 확인용
class FunctionClass {
private title : string;
private author : string;
static staticalMember : number = 100;
constructor() {
this.title = "private 으로 선언된 title";
this.author = "private 으로 선언된 author";
}
}
javascript
-- tsc
// 클래스 컴파일 결과 확인용
var FunctionClass = /** @class */ (function () {
function FunctionClass() {
this.title = "private 으로 선언된 title";
this.author = "private 으로 선언된 author";
}
FunctionClass.staticalMember = 100;
return FunctionClass;
}());
이상하다? private 이 적용되지 않는데?
먼저 들었던 생각이다.
나는 분명히 private
을 선언하고, 해당 인스턴스 변수를
콘솔로 출력하려고 한다면, 에러가 떴다.
그런데, 자바스크립트로 출력된 내용은 완전히 public
이나 다름이 없었다.
즉, TypeScript 에서는 문법적으로 보호 해 주지만, 컴파일 된 자바스크립트는 그렇지 않다는 것이다.
이쯤되니, 이러한 생각이 들었다.
어플리케이션의 실행은 주로 타입스크립트의 내용이 해석된 dist
or 다양한 이름을 가진 폴더에서 실행하는데,
dist
의 내용에 JavaScript 로, private 선언을 했던 객체에 직접적인 접근이 가능하다는 것이다.
그렇다면, 코드 내부적으로 JavaScript 의 보안을 지키기는 어렵다고 생각했다.
따라서, 나는 TypeScript 를 사용하면서도, JavaScript 로 번역되었을 때,
private
으로 선언된 변수들이 컴파일 되더라도 보안을 유지하는 방식이 궁금했다.
방대한 NPM 의 세계만큼, JavaScript 도 방법이 있을 거라고 확신했다.
JavaScript 에서 내부적인 프로퍼티는 어떻게 보호할까?
우선, JavaScript 자체적으로 private 을 사용할 수 있게 해 주기도 한다.
그런데, 자체 빌트인으로 사용하는 것이 아니라, 타입스크립트 상으로 target : 2015
이상의 문법을 사용해야 한다.
Example : javascript
class TestPrivate {
#title;
constructor() {
this.#title = "private title";
}
get title() {
return this.#title;
}
}
const instance = new TestPrivate();
console.log(instance.title);
console.log(instance["#title"]);
Result :
$ node test-private.js
private title # instance.title
undefined # instance["#title"]
바닐라 자바스크립트로 작성했을 때, 현재 "25-3-30" 기준으로, 명세서에 등재되기 직전 상태라고 한다.
따라서, 잘 작동하고 있다.
그런데, 자바스크립트에서 이런 말이 있었다.
JavaScript 에서의 class 문은 "문법적 설탕" 이다.
따라서, 이것이 어떻게 원초적으로 작성되어 있을지 보기로 했다.
typescript
:
class TsPrivate {
#title : string;
constructor() {
this.#title = "TypeScript Private Title";
}
}
const TsInstance = new TsPrivate();
javascript
:
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var _TsPrivate_title;
var TsPrivate = /** @class */ (function () {
function TsPrivate() {
_TsPrivate_title.set(this, void 0);
__classPrivateFieldSet(this, _TsPrivate_title, "TypeScript Private Title", "f");
}
return TsPrivate;
}());
_TsPrivate_title = new WeakMap(); // 여기에 집중
var TsInstance = new TsPrivate();
내부 프로퍼티를 위한 강력한 은닉화 방법으로 WeakMap
을 사용하는 것을 볼 수 있었다.
그러나, 실제 브라우저나 Node.js 환경 (es6 +) 에서, #
접두어를 붙여 private 를 전부 구현 할 수 있다.
컴파일 과정에서 WeakMap
이 나오는 이유는, 노후화 된 모듈과도 연결될 수 있기 때문에
아직 타입스크립트 private 구문이 WeakMap
을 이용하여 컴파일되는 것이 아닐까 조심스럽게 예측해 본다.
프로퍼티 은닉화 방법들
1. Underscore - _
컨벤션으로서 "이 변수는 private 처럼 운용하니까, 추출하지 마세요" 를 의미한다.
class TestClass {
_title : string;
constructor(title : string) {
this._title = title;
}
getTitle() : string {
return this._title;
}
setTitle(title : string) {
this._title = title;
}
}
const testInstance = new TestClass("Custom Title");
console.log(testInstance.getTitle());
console.log(testInstance._title);
Result :
$ ts-node class-and-function.js
Custom Title # console.log(testInstance.getTitle());
Custom Title # console.log(testInstance._title);
_
(언더스코어) 로 작성된 변수는, 같은 프로젝트를 작성하는 개발자에게 있어
직접적으로 건드리지 않는 변수라고 알리는 것과 동일하다.
하지만, 결국 인스턴스를 통해 바로 직접적으로 접근이 가능하다.
WeakMap
먼저, WeakMap 이 (es6) - 2015 년에 도입되었다고 한다.. 따라서 지금부터 es6 로 바꾼다!!
JavaScript 의 일반적인 Map
과 달리, 객체 혹은 등록되지 않은 Symbol 만 키로 삼을 수 있다.
여기서 "등록되지 않은 Symbol" 이란,
Symbol.for("None Registered Symbol!")
와 같이,
전역 상태로 등록된 심볼을 의미한다.
일단 객체에서는 WeakMap
의 키로 "자기 자신" 을 넣으며,
값으로는 private
으로 사용할 값들을 객체 내부로 넣는다.
Example : typescript
const dataStore = new WeakMap<object, any>();
class TestClass {
constructor(pValue1 : number, pValue2 : number) {
dataStore.set(this, {
pValue1,
pValue2
});
}
getPrivateValue1() : number {
const data = dataStore.get(this);
const value1 = data.pValue1;
return value1;
}
plusPrivateValue1() : void {
const data = dataStore.get(this);
const value1 = data.pValue1 + 1;
dataStore.set(this, {...data, pValue1 : value1})
}
getPrivateValue2() : number {
const data = dataStore.get(this);
const value2 = data.pValue2;
return value2;
}
plusPrivateValue2() : void {
const data = dataStore.get(this);
const value2 = data.pValue2 + 1;
dataStore.set(this, {...data, pValue2 : value2});
}
}
const testInstance = new TestClass(1, 10);
// 1 번째 private 변수 관리
console.log(testInstance.getPrivateValue1());
testInstance.plusPrivateValue1();
console.log(testInstance.getPrivateValue1());
// 2 번째 private 변수 관리
console.log(testInstance.getPrivateValue2());
testInstance.plusPrivateValue2();
console.log(testInstance.getPrivateValue2());
Result :
$ ts-node class-and-function.ts
# $ tsc && node class-and-function.js
1
2
10
11
보는 것 처럼, WeakMap
에 저장된 private
한 변수들을 가져와서,
내부에서만 건드릴 수 있게 만들어 놓았다.
그런데, 이것도 "완전히 보안" 은 아니다.
왜냐하면 WeakMap
의 key 부분은 인스턴스로 접근이 가능한데,
"현재 파일 코드 영역" 에서만 접근이 가능하다.
만약 dataStore.get(this)
로 접근한다면,
여전히 TestClass
인스턴스의 private 변수에 접근이 가능하다.
그럼에도 불구하고 WeakMap
이 좋은 보안을 지니는 이유가,
내부 프로퍼티 추출 시 어떠한 private
처럼 운용하는 변수가 나오지 않기 때문이다.
우리는 전역 스코프에 존재하는 WeakMap
에 private
변수들을 넣어놨지,
클래스에 선언하지 않았다. 이로서 어느정도 캡슐화에는 성공한 것이다.
하지만 다시 생각 해 보자.
내가 만든 이 클래스는, 이 파일에서만 사용되는 것이 아니라, 외부에서 가져와 사용 할 수 있다.
만약에 TestClass
가 export class TestClass
라면,
외부에서 접근 할 때, 파일 스코프로 선언된 const dataStore
은 접근 할 수가 없다!
Symbol
아까 위에서 WeakMap
선언을 "파일 스코프" 로 만들고,
클래스 내부에서 해당 위크맵에 인스턴스 스스로를 등록하여 은닉하도록 만들었다.
Symbol
사용 또한 동일하다.
// private 데이터 2개 선언.
const pData1 = Symbol("123");
const pData2 = Symbol("456");
class TestClass {
// 내부에 심볼 데이터로 넣기
[pData1] : number;
[pData2] : string;
constructor() {
this[pData1] = 10;
this[pData2] = "123";
}
getPData1PublicMethod() {
return this[pData1];
}
getPData2PublicMethod() {
return this[pData2];
}
}
// 인스턴스 생성
const testInstance = new TestClass();
console.log(testInstance.getPData1PublicMethod());
console.log(testInstance.getPData2PublicMethod());
// 객체 내부에 존재하는 모든 심볼의 목록을 가져온다.
const instanceSymbol1 = Object.getOwnPropertySymbols(testInstance)[0].valueOf()
// 첫 번째 심볼을 출력
console.log(instanceSymbol1);
// 첫 번째 심볼로 인스턴스 내부의 private 한 변수에 접근이 가능하다.
console.log(testInstance[instanceSymbol1]);
Result :
$ tsc && node class-and-function.js
10 # testInstance.getPData1PublicMethod()
123 # testInstance.getPData2PublicMethod()
Symbol(123) # instanceSymbol1
10 # testInstance[instanceSymbol1]
하지만, 문제가 있다면, Object
라는 전역 객체가 인스턴스 내부의 심볼들을 가져올 수 있다.
이를 이용하여, 인스턴스 내부에서 변수들을 private
화 시키는 심볼들이 추출된다.
마지막에 보이는 것 처럼, 다시 인스턴스에 심볼을 넣어 내부 정보를 가져올 수 있다는 단점이 있다.
파일 스코프로 인해 직접적으로 심볼을 가져올 수는 없지만,
결국 private 데이터를 가져올 수 있다.
Closure
사실상 클로저가 가장 private 에 가깝지 않을까 생각된다.
"TOAST UI" 사이트에서 말하길, 모듈 패턴으로 구성되며,
이는 ES6 모듈을 사용하기 시작한 이래로 점차 사용하지 않는 형태라고 한다.
function TestModule() {
const privateValue = "It is Private value";
const publicGetPrivateValue = () => {
return privateValue;
}
function functionGetValue () {
return privateValue;
}
return {
publicGetPrivateValue
}
}
// TestModule 형태
console.log(TestModule());
// 클로저 내부의 private 값을 조회한다.
console.log(TestModule().publicGetPrivateValue());
// Object 로도 조회할 수 있는 프로퍼티는 return 시킨 메서드 뿐이다.
console.log(Object.getOwnPropertyNames(TestModule()));
Result :
➜ tempTs tsc && node class-and-function.js
{ publicGetPrivateValue: [Function: publicGetPrivateValue] } # TestModule()
It is Private value # TestModule().publicGetPrivateValue()
[ 'publicGetPrivateValue' ] # Object.getOwnPropertyNames(TestModule())
보다시피, 함수 자체에서 return
하지 않은 프로퍼티와 메서드들은 노출이 되지 않는다.
이는 WeakMap
을 사용하는 캡슐화 방식보다도 훨씬 더 강하게 캡슐화하고 있다.
만약에 return
문을 없애버리면, 내부 함수에 존재하는 어떠한 함수 혹은 프로퍼티도 접근할 수 없다.
Private Class Field - #
이번에는 클래스를 사용하고도 private
한 프로퍼티들을 접근할 수 없는 방식이다.
ES2019
부터 #
을 붙이면, 외부에서 접근 할 수 없게 되었다.
//
// // 클래스 컴파일 결과 확인용
// class FunctionClass {
// #title : string;
// #author : string;
// static staticalMember : number = 100;
//
// constructor() {
// this.#title = "private 으로 선언된 title";
// this.#author = "private 으로 선언된 author";
// }
//
// getTitle() : string {
// return this.#title;
// }
// setTitle(title : string) {
// this.#title = title;
// }
// }
/*
const dataStore = new WeakMap<object, any>();
class TestClass {
constructor(pValue1 : number, pValue2 : number) {
dataStore.set(this, {
pValue1,
pValue2
});
}
getPrivateValue1() : number {
const data = dataStore.get(this);
const value1 = data.pValue1;
return value1;
}
plusPrivateValue1() : void {
const data = dataStore.get(this);
const value1 = data.pValue1 + 1;
dataStore.set(this, {...data, pValue1 : value1})
}
getPrivateValue2() : number {
const data = dataStore.get(this);
const value2 = data.pValue2;
return value2;
}
plusPrivateValue2() : void {
const data = dataStore.get(this);
const value2 = data.pValue2 + 1;
dataStore.set(this, {...data, pValue2 : value2});
}
}
const testInstance = new TestClass(1, 10);
// 1 번째 private 변수 관리
console.log(testInstance.getPrivateValue1());
testInstance.plusPrivateValue1();
console.log(testInstance.getPrivateValue1());
// 2 번째 private 변수 관리
console.log(testInstance.getPrivateValue2());
testInstance.plusPrivateValue2();
console.log(testInstance.getPrivateValue2());
const testInstance2 = new TestClass(5, 15);
console.log(dataStore.get(testInstance));
*/
/*
// private 데이터 2개 선언.
const pData1 = Symbol("123");
const pData2 = Symbol("456");
class TestClass {
// 내부에 심볼 데이터로 넣기
[pData1] : number;
[pData2] : string;
constructor() {
this[pData1] = 10;
this[pData2] = "123";
}
getPData1PublicMethod() {
return this[pData1];
}
getPData2PublicMethod() {
return this[pData2];
}
}
// 인스턴스 생성
const testInstance = new TestClass();
console.log(testInstance.getPData1PublicMethod());
console.log(testInstance.getPData2PublicMethod());
// 객체 내부에 존재하는 모든 심볼의 목록을 가져온다.
const instanceSymbol1 = Object.getOwnPropertySymbols(testInstance)[0].valueOf()
// 첫 번째 심볼을 출력
console.log(instanceSymbol1);
// 첫 번째 심볼로 인스턴스 내부의 private 한 변수에 접근이 가능하다.
console.log(testInstance[instanceSymbol1]);
console.log(Object.getOwnPropertyNames(testInstance));
*/
/*
function TestModule() {
const privateValue = "It is Private value";
const publicGetPrivateValue = () => {
return privateValue;
}
function functionGetValue () {
return privateValue;
}
return {
publicGetPrivateValue
}
}
// TestModule 형태
console.log(TestModule());
// 클로저 내부의 private 값을 조회한다.
console.log(TestModule().publicGetPrivateValue());
// Object 로도 조회할 수 있는 프로퍼티는 return 시킨 메서드 뿐이다.
console.log(Object.getOwnPropertyNames(TestModule()));
*/
class TestClass {
#pValue1 : number;
#pValue2 : number;
constructor() {
this.#pValue1 = 10;
this.#pValue2 = 20;
}
getPrivateValues() : number[] {
return [this.#pValue1, this.#pValue2];
}
#setPValue1(pValue1 : number) {
this.#pValue1 = pValue1;
}
#setPValue2(pValue2 : number) {
this.#pValue2 = pValue2;
}
setPrivateValues(value1 : number, value2 : number) {
this.#setPValue1(value1);
this.#setPValue2(value2);
}
}
const testInstance = new TestClass();
console.log(testInstance.getPrivateValues());
testInstance.setPrivateValues(1, 2);
console.log(testInstance.getPrivateValues());
console.log(Object.getOwnPropertyNames(testInstance));
Result :
$ tsc && node class-and-function.js
[ 10, 20 ] # testInstance.getPrivateValues()
[ 1, 2 ] # testInstance.getPrivateValues()
[] # Object.getOwnPropertyNames(testInstance)
보다시피, 클래스 내부에 선언되는 인스턴스 프로퍼티들은 전부 #
접두어(prefix) 를 가진다.
이는 ES2019
부터 추가된 스펙으로, C++, Java 와 같이 클래스 private
처럼 취급해 준다.
배운 점
이번에 JavaScript, TypeScript 에서 어떻게 private
을 구현 할 수 있을까?
이것을 작성하게 된 이유는 바로, TypeScript
의 private
구문을 가진 코드가
컴파일 된 이후, 자바스크립트에서 실제로 보안 처리가 되지 않았기 때문이었다.
JavaScript 를 열심히 사용 해 온 사람들이
클래스 내부의 프로퍼티와 함수들을 private 화 시키기 위해 해 온 노력들을 알게 되었다.
웹 사이트에서 사용하던 언어에서, 인지도 상승과 최적화를 거치며 영역이 넓어진 JS 가,
입문은 쉽지만, 깊은 이해는 여타 다른 언어와 비슷하게 배워야 할 것이 많다고 생각한다.
이 다음 Node.js 에 대한 내용은 아마 다중 스레드를 다루는 worker
API 를 습득 할 것인데,
하나의 쓰레드에 비동기적 실행을 채택한 Node.js 가 어떻게 쓰레드를 사용하는지 배워 볼 것이다.
참고 사이트
MDN Symbol
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Symbol
MDN WeakMap
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
Tistory 프론트엔드 개발자 블로그
모던 JavaScript 튜토리얼
https://ko.javascript.info/private-protected-properties-methods
TOAST UI 포스팅 - NHN 에서 개발한 UI 오픈소스 라이브러리