자바스크립트에서 Private과 친해지기.

동기

  • 우아한테크코스 Level 1, 자동차 경주 게임 1단계 과정 중 클래스 내부에서만 사용하는 메서드와 변수를 구분하기 위해 언더바를 활용하여 비공개 멤버를 구분.
  • 이후 피드백을 통해 "언더바를 활용한 비공개 멤버 구분을 지양하라"라는 내용 확인.

과정

1. 왜 사용하면 안될까?

  • IDE가 발달하기 전에 쓰던 머나먼 옛날(2008년)의 "약속"이고, 현재에 들어서는 IDE의 발달로 소스 트리를 간편하게 볼 수 있고 VSCode의 다양한 확장 프로그램들으로 구분이 어렵지 않다.

  • 개발자들의 "약속"일뿐이지, 실제로 은닉이 되지 않고 외부에서 손쉽게 접근할 수 있다. 접근이 가능하다면 추후 유지/보수 과정에서 분명 접근하는 케이스가 생길 것이다!

  • 시간이 지나서 아래와 같은 더 나은 방법이 등장하고 있다.


2. 그렇다면 대체제는?

1. 함수에서는 클로저의 특징 활용

const counter = () => {
    let privateNumber = 0;
    const changeBy = (value) => {
        privateNumber += value;
    }
    
    return {
        increment: () => {
            changeBy(1);
        },
        decrement: () => {
            changeBy(-1);
        },
        value: () => privateNumber
    }
}

console.log(counter.value()); // 0
counter.increment();
console.log(counter.value()); // 1
counter.decrement();
console.log(counter.value()); // 0

console.log(counter.privateNumber); // undefined

문제 탐색

  • 해당 객체를 여러개 만든다면, 생성될 때마다 서로 다른 클로저 함수가 만들어져 메모리 관리에 좋지 않다.

2. ES2015의 Symbol()을 활용하여 이름을 은닉하라

const privateNumber = Symbol('여러분은 지금 변수의 이름을 비공개 하여 접근을 막는 과정을 보고 계십니다.');
class Counter {
  constructor() {
    this[privateNumber] = 0;
  }

  increment() {
    this[privateNumber] += 1;
  }

  decrement() {
    this[privateNumber] -= 1;
  }

  value() {
    return this[privateNumber];
  }
}

const counter = new Counter();
counter.increment()
console.log(counter.value()); // 1

counter.decrement()
console.log(counter.value()); // 0

문제 탐색

  • ES6의 import/export 방식을 통해서만 비공개 멤버를 사용할 수 있다. 다른 파일에서는 Symbol으로 선언된 privateNumber 변수를 export 하지 않는 한 접근할 수 없지만, 반대로 내부 파일에 다른 코드가 있을 시 접근이 가능해진다.
  • Object.getOwnPropertySymbols(Object) 방식으로 심볼의 키를 얻을 수 있기에 실제로 접근이 완전히 불가능한 것은 아니다.

    console.log(Object.getOwnPropertySymbols(counter)); // Array [ Symbol("여러분은 지금 변수의 이름을 비공개 하여 접근을 막는 과정을 보고 계십니다.") ]


3. 기다리고, 기다린 "#"

  • 현재 TC39 Stage 3 상태인, 클래스에서 변수 또는 메서드에 #을 Prefix로 추가해 비공개 멤버를 선언할 수 있게 되었다.
class counter {
    #privateNumber = 0;
    
    increment() {
        this.#privateNumber += 1;
    }
    
    decrement() {
        this.#privateNumber -= 1;
    }
    
    value() {
        return this.#privateNumber;
    }
}

const counter = new Counter();
counter.increment()
console.log(counter.value()); // 1
console.log(counter.#privateNumber); // undefined

counter.decrement()
console.log(counter.value()); // 0

3-2. 하지만! ESLint에서는 계속 문법 오류가 뜬다? EsLint에 # 붙이기.

  • 아쉽게도 ESLint는 TC39 후보 상태(스테이지 3)의 기능은 지원하지 않는다.

    TC39의 스테이지 3 후보 단계는 완성 단계에 가깝고, 구현 주체나 사용자들로부터 피드백을 좀 더 받아보는 일만이 남은 상태다.
    그렇기에 표준에 포함되어도, 그 전에 심각한 문제가 발견되지 않는 이상 문법, 동작 방식은 변경이 없는 편이다.

  • 하지만 아래와 같이 패키지를 설치하고, ESLint 설정과 바벨 설정을 수정하면 ESLint에서도 TC39 상태의 기능들을 허용할 수 있다.

패키지 설치

npm i @babel/core @babel/eslint-parser @babel/preset-env -D

ESLint 설정 파일(.eslintrc)에서 parser 옵션 설정

{
    "env": {
        "browser": true,
        "es2021": true,
        "node": true
    },
    "extends": "eslint:recommended",
    **"parser": "@babel/eslint-parser",**
    "parserOptions": {
        "ecmaVersion": 12,
        "sourceType": "module"
    },
    "rules": {
    }
}

babel 구성 파일(.babelrc) 설정

{
  "presets": [
    ["@babel/preset-env",
    {
      "shippedProposals": true
    }]
  ]
}

결과

  • 자동차 경주 게임 2단계에서는 TC39 Stage 3의 Private class(#)를 사용하여 비공개 메서드와 변수를 구현을 진행.
  • 3-2 단계를 진행하여 ESLint를 활성화하여도 문법 오류가 더 이상 발생되지 않음.
  • "#"을 활용한 Private 처리는 최신 브라우저들 대부분을 지원하지만, 브라우저의 호환성을 많이 고려하여야 하는 환경이라면 클로저 사용을 권장.

참조

틀린 내용, 피드백이 있으시다면 댓글 남겨주시면 감사하겠습니다! 🙇‍♂️

좋은 웹페이지 즐겨찾기