JavaScript 네이밍 컨벤션: Public vs Private
개요
JavaScript/TypeScript에서 클래스의 메서드나 프로퍼티 이름을 짓는 방식은 접근 권한(public/private)에 따라 달라집니다.
Public 메서드: 언더스코어 없이
외부에서 호출할 수 있는 메서드는 언더스코어 없이 명확한 이름을 사용합니다.
class InfiniteTextScroller {
// ✅ Public static 메서드 - 외부 API
static create(options) {
// 사용자가 직접 호출
return this.#createInternal(options);
}
// ✅ Public 인스턴스 메서드
start() {
console.log('Starting...');
}
destroy() {
console.log('Destroying...');
}
}
// 사용 예시
const scroller = InfiniteTextScroller.create({ id: 'box1' });
scroller.start();
scroller.destroy();
특징:
- 외부 사용자가 호출하도록 의도된 메서드
- 명확하고 간결한 이름 사용
- 클래스의 공개 API (Public API)
- 라이브러리나 모듈의 인터페이스
Private 메서드: # 또는 _ 접두사
내부에서만 사용하는 메서드는 private 표시를 추가합니다.
1. # 방식 (Modern JavaScript - 권장)
class DataProcessor {
// ✅ 진짜 private - 외부에서 접근 불가
static #validateData(data) {
return Array.isArray(data);
}
#privateField = 'secret';
static process(data) {
if (!this.#validateData(data)) {
throw new Error('Invalid data');
}
return data.map(item => item * 2);
}
}
// 접근 테스트
DataProcessor.process([1, 2, 3]); // ✅ 정상 동작
DataProcessor.#validateData([1, 2, 3]); // ❌ SyntaxError: Private field
특징:
- 진짜 private (언어 차원에서 강제)
- ES2022부터 지원
- 클래스 외부에서 절대 접근 불가
- 모던 JavaScript의 표준
2. _ 방식 (Legacy JavaScript)
class DataProcessor {
// ⚠️ 관례상 private (실제로는 접근 가능)
static _processInternal(data) {
return data.map(item => item * 2);
}
_privateField = 'secret';
static process(data) {
return this._processInternal(data);
}
}
// 접근 테스트
DataProcessor.process([1, 2, 3]); // ✅ 정상 동작
DataProcessor._processInternal([1, 2, 3]); // ⚠️ 동작하지만 사용하면 안됨
특징:
- 관례상 private을 나타냄
- 실제로는 외부에서 접근 가능 (강제되지 않음)
- 레거시 코드나 프레임워크에서 자주 보임
- “이 메서드는 내부용이니 사용하지 마세요”라는 신호
3. TypeScript private 키워드
class FileManager {
// public (기본값)
public upload(file: File) {
return this.validateFile(file);
}
// protected - 자식 클래스에서만 접근 가능
protected validateFile(file: File): boolean {
return file.size < 1024 * 1024;
}
// private - 이 클래스 내부에서만 접근 가능
private logUpload(fileName: string) {
console.log(`Uploaded: ${fileName}`);
}
// private static
private static instance: FileManager;
}
특징:
- 명시적인 접근 제어 (
public,protected,private) - IDE와 컴파일러가 잘못된 접근 방지
- 코드 가독성 향상
- 주의: JavaScript로 컴파일되면 실제 private이 아님 (런타임 보호 없음)
실전 예시: Multiton 패턴
class InfiniteTextScroller {
// ✅ Public static 프로퍼티 (의도적으로 노출)
static instances = new Map();
// ✅ Private static 헬퍼 메서드 (구현 세부사항)
static #validateOptions(options) {
if (!options.containerId) {
throw new Error('containerId is required');
}
return true;
}
// ✅ Private static 초기화 메서드
static #initializeScroller(options) {
return {
id: options.containerId,
element: document.getElementById(options.containerId),
config: options
};
}
// ✅ Public static 팩토리 메서드 (외부에서 호출)
static create(options) {
this.#validateOptions(options);
const containerId = options.containerId;
if (this.instances.has(containerId)) {
return this.instances.get(containerId);
}
const instance = this.#initializeScroller(options);
this.instances.set(containerId, instance);
return instance;
}
// ✅ Public static 유틸리티 메서드
static getInstance(containerId) {
return this.instances.get(containerId);
}
// ✅ Public static 정리 메서드
static destroyAll() {
this.instances.clear();
}
}
// 사용 예시
const scroller = InfiniteTextScroller.create({ containerId: 'box1' });
// InfiniteTextScroller.#validateOptions({ id: 'box1' }); // ❌ Error
네이밍 가이드라인
| 상황 | 방식 | 예시 |
|---|---|---|
| 외부 API | 언더스코어 없음 | create(), getInstance(), destroy() |
| 내부 헬퍼 (Modern JS) | # 접두사 |
#validateOptions(), #initializeScroller() |
| 내부 헬퍼 (Legacy JS) | _ 접두사 |
_processData(), _handleEvent() |
| TypeScript | private 키워드 |
private validateData() |
| 의도적 노출 (유틸) | 언더스코어 없음 | static instances (공개적으로 접근 가능) |
왜 _create를 사용하는 사람들이 있을까?
패턴 1: Public/Private 분리
class ComponentFactory {
// Public API - 검증, 로깅 담당
static create(type, options) {
console.log(`Creating ${type}...`);
// 유효성 검사
if (!options) {
throw new Error('Options required');
}
return this._create(type, options);
}
// Private 구현 - 실제 생성 로직
static _create(type, options) {
switch(type) {
case 'button': return new Button(options);
case 'input': return new Input(options);
default: throw new Error('Unknown type');
}
}
}
이유:
- 관심사 분리: public 메서드는 검증/로깅, private 메서드는 실제 로직
- 테스트 편의성: 내부 로직만 따로 테스트하고 싶을 때 (권장하지 않음)
- 레거시 습관:
#이 나오기 전에는_만 사용 가능했음
패턴 2: 현대적(?) 대안 (권장)
class ComponentFactory {
static create(type, options) {
console.log(`Creating ${type}...`);
if (!options) {
throw new Error('Options required');
}
return this.#createComponent(type, options); // ✅ #으로 대체
}
static #createComponent(type, options) {
switch(type) {
case 'button': return new Button(options);
case 'input': return new Input(options);
default: throw new Error('Unknown type');
}
}
}
최종 권장사항
Modern JavaScript (ES2022+)
class MyClass {
// ✅ Public - 언더스코어 없이
static create(options) {
return this.#initialize(options);
}
// ✅ Private - # 사용
static #initialize(options) {
return new MyClass(options);
}
static #helper() {
// 내부 헬퍼 메서드
}
#privateField = 'secret';
}
TypeScript
class MyClass {
// ✅ Public (명시적 또는 생략)
public static create(options: Options) {
return this.initialize(options);
}
// ✅ Private
private static initialize(options: Options) {
return new MyClass(options);
}
private privateField = 'secret';
}
Legacy JavaScript (ES5/ES6)
class MyClass {
// ✅ Public
static create(options) {
return this._initialize(options);
}
// ⚠️ Private (관례)
static _initialize(options) {
return new MyClass(options);
}
constructor() {
this._privateField = 'secret';
}
}
핵심 정리
기본 원칙
| 구분 | 네이밍 | 접근성 | 예시 |
|---|---|---|---|
| Public | 언더스코어 없음 | 외부에서 호출 가능 | create(), getInstance() |
| Private (Modern) | # 접두사 |
외부 접근 불가 | #validateData() |
| Private (Legacy) | _ 접두사 |
외부 접근 가능 (관례상 금지) | _processInternal() |
| Private (TS) | private 키워드 |
컴파일 타임 체크 | private helper() |
선택 가이드
// ✅ 외부 사용자가 호출하는 메서드
static create(options) { }
getInstance() { }
destroy() { }
// ✅ 내부 구현 세부사항 (Modern JS)
static #validateOptions(options) { }
#initializeState() { }
// ✅ 내부 구현 세부사항 (TypeScript)
private static validateOptions(options: Options) { }
private initializeState() { }
// ⚠️ 내부 구현 세부사항 (Legacy - 마이그레이션 권장)
static _validateOptions(options) { }
_initializeState() { }
실무 팁
- 새 프로젝트:
#사용 (Modern JavaScript) - TypeScript:
private키워드 사용 - 레거시 유지보수: 기존
_패턴 유지, 점진적으로#로 마이그레이션 - 라이브러리 개발: Public API는 명확하게, Private은 철저히 숨김
- 팀 컨벤션: 팀 내 일관성 유지가 가장 중요
절대 규칙
- ✅ Public API는 깔끔한 이름 사용
- ✅ Private 메서드는 숨김 표시 필수
- ✅ 한 프로젝트 내에서 일관성 유지
- ❌ Public 메서드에
_또는#사용 금지 - ❌ Private과 Public을 혼용하지 말 것
비교표: 각 방식의 장단점
| 방식 | 진짜 Private | 브라우저 지원 | 가독성 | 추천도 |
|---|---|---|---|---|
# (Modern JS) |
✅ Yes | Modern browsers (2022+) | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
private (TS) |
❌ No (컴파일 후) | All (컴파일됨) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
_ (Legacy) |
❌ No | All | ⭐⭐⭐ | ⭐⭐ |
| WeakMap | ✅ Yes | ES6+ | ⭐⭐ | ⭐⭐⭐ |
| Symbol | ⚠️ 유사 Private | ES6+ | ⭐⭐ | ⭐⭐ |
Private 구현 방식
1. WeakMap을 이용한 Private 데이터 저장
#이 나오기 전 진짜 private을 구현하는 방법으로, 외부에서 절대 접근할 수 없습니다.
const privateData = new WeakMap();
const privateCounter = new WeakMap();
class BankAccount {
constructor(balance) {
// WeakMap에 private 데이터 저장
privateData.set(this, {
balance: balance,
accountNumber: Math.random().toString(36).substring(7)
});
privateCounter.set(this, 0);
}
deposit(amount) {
const data = privateData.get(this);
data.balance += amount;
privateCounter.set(this, privateCounter.get(this) + 1);
return data.balance;
}
getBalance() {
return privateData.get(this).balance;
}
getTransactionCount() {
return privateCounter.get(this);
}
}
const account = new BankAccount(1000);
console.log(account.getBalance()); // 1000
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.getTransactionCount()); // 1
// ❌ 외부에서 접근 불가
console.log(account.balance); // undefined
console.log(Object.keys(account)); // []
장점:
- ✅ 진짜 private (외부 접근 불가)
- ✅
#문법이 없던 시절의 표준 패턴 - ✅ 메모리 누수 방지 (인스턴스 삭제 시 자동 정리)
단점:
- ❌ 코드 복잡도 증가
- ❌ 가독성 저하
- ❌ 디버깅 어려움
- ❌ 성능 오버헤드 (Map 조회 비용)
언제 사용?
#문법을 사용할 수 없는 환경 (레거시 브라우저)- 진짜 private이 필요하지만 Babel/TypeScript 없이 순수 ES6만 사용 가능한 경우
2. Symbol을 이용한 유사 Private
완전한 private은 아니지만, 일반적인 접근을 어렵게 만듭니다.
const _balance = Symbol('balance');
const _accountNumber = Symbol('accountNumber');
const _validateAmount = Symbol('validateAmount');
class BankAccount {
constructor(balance) {
this[_balance] = balance;
this[_accountNumber] = Math.random().toString(36).substring(7);
}
[_validateAmount](amount) {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
return true;
}
deposit(amount) {
this[_validateAmount](amount);
this[_balance] += amount;
return this[_balance];
}
getBalance() {
return this[_balance];
}
getAccountNumber() {
return this[_accountNumber];
}
}
const account = new BankAccount(1000);
console.log(account.getBalance()); // 1000
// ⚠️ 일반적인 방법으로는 접근 불가
console.log(account.balance); // undefined
console.log(Object.keys(account)); // []
// ❌ 하지만 Symbol을 얻으면 접근 가능
console.log(Object.getOwnPropertySymbols(account));
// [Symbol(balance), Symbol(accountNumber)]
const symbols = Object.getOwnPropertySymbols(account);
console.log(account[symbols[0]]); // 1000 (접근됨!)
장점:
- ✅ 일반적인 접근 방지 (
Object.keys(),for...in등에서 숨김) - ✅ 프로퍼티 충돌 방지
- ✅ ES6부터 사용 가능
단점:
- ❌ 진짜 private 아님 (
Object.getOwnPropertySymbols()로 접근 가능) - ❌ JSON 직렬화 시 제외됨
- ❌ 가독성이 떨어짐
언제 사용?
- 프로퍼티 이름 충돌을 피하고 싶을 때
- 공개 API에서 숨기고 싶지만 완전한 private은 불필요할 때
- 메타데이터나 내부 설정 저장 시
3. Private Static 블록 (ES2022)
클래스 로딩 시 한 번만 실행되는 static 초기화 블록입니다.
class DatabaseConnection {
static #config;
static #connections = new Map();
static #maxConnections = 10;
// static 초기화 블록 - 클래스 정의 시 한 번만 실행
static {
console.log('Initializing DatabaseConnection class...');
// 설정 로드
this.#config = {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
timeout: 30000
};
// 초기 연결 풀 생성
for (let i = 0; i < 3; i++) {
this.#connections.set(i, {
id: i,
status: 'idle',
createdAt: new Date()
});
}
console.log(`Initialized with ${this.#connections.size} connections`);
}
static #validateConnectionLimit() {
if (this.#connections.size >= this.#maxConnections) {
throw new Error('Connection pool exhausted');
}
}
static getConnection(id) {
return this.#connections.get(id);
}
static createConnection() {
this.#validateConnectionLimit();
const newId = this.#connections.size;
const connection = {
id: newId,
status: 'idle',
createdAt: new Date()
};
this.#connections.set(newId, connection);
return connection;
}
static getConfig() {
return { ...this.#config }; // 복사본 반환
}
static getConnectionCount() {
return this.#connections.size;
}
}
// 클래스 정의 시 static 블록 자동 실행됨
// Console: "Initializing DatabaseConnection class..."
// Console: "Initialized with 3 connections"
console.log(DatabaseConnection.getConnectionCount()); // 3
console.log(DatabaseConnection.getConfig()); // { host: 'localhost', port: 5432, ... }
const conn = DatabaseConnection.createConnection();
console.log(conn); // { id: 3, status: 'idle', ... }
장점:
- ✅ 복잡한 static 초기화 로직 처리
- ✅ Private static 필드 초기화 가능
- ✅ 클래스 로딩 시 한 번만 실행 (효율적)
- ✅ try-catch로 초기화 에러 처리 가능
단점:
- ❌ ES2022부터 지원 (최신 기능)
- ❌ 일부 레거시 환경에서 미지원
언제 사용?
- 복잡한 static 설정 초기화
- 환경 변수 로드 및 검증
- Singleton 패턴의 초기 설정
- 연결 풀, 캐시 등 리소스 사전 초기화
// 여러 static 블록 사용 가능
class Configuration {
static #env;
static #features;
static #cache;
static {
// 환경 설정 로드
this.#env = {
mode: process.env.NODE_ENV || 'development',
debug: process.env.DEBUG === 'true'
};
console.log('Environment loaded');
}
static {
// 기능 플래그 초기화
this.#features = {
newUI: this.#env.mode === 'development',
analytics: this.#env.mode === 'production'
};
console.log('Features configured');
}
static {
// 캐시 초기화
this.#cache = new Map();
console.log('Cache initialized');
}
static getEnv() {
return { ...this.#env };
}
static isFeatureEnabled(name) {
return this.#features[name] || false;
}
}
// 순서대로 실행됨:
// Console: "Environment loaded"
// Console: "Features configured"
// Console: "Cache initialized"
4. Getter/Setter와 Private 필드
Private 필드를 Getter/Setter로 안전하게 노출하는 패턴입니다.
class Temperature {
#celsius;
constructor(celsius) {
this.#celsius = celsius;
}
// Getter - 읽기 전용 접근
get celsius() {
return this.#celsius;
}
// Setter - 유효성 검증 포함
set celsius(value) {
if (typeof value !== 'number') {
throw new TypeError('Temperature must be a number');
}
if (value < -273.15) {
throw new RangeError('Temperature below absolute zero');
}
this.#celsius = value;
}
// Computed property with getter
get fahrenheit() {
return this.#celsius * 9/5 + 32;
}
set fahrenheit(value) {
this.celsius = (value - 32) * 5/9; // celsius setter를 통해 검증
}
get kelvin() {
return this.#celsius + 273.15;
}
set kelvin(value) {
this.celsius = value - 273.15;
}
}
const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15
temp.fahrenheit = 86;
console.log(temp.celsius); // 30
// ❌ 직접 접근 불가
console.log(temp.#celsius); // SyntaxError
// ✅ Setter 검증 동작
temp.celsius = -300; // RangeError: Temperature below absolute zero
패턴: 읽기 전용 프로퍼티
class User {
#id;
#createdAt;
#name;
constructor(name) {
this.#id = Math.random().toString(36).substring(7);
this.#createdAt = new Date();
this.#name = name;
}
// 읽기 전용 (getter만 제공)
get id() {
return this.#id;
}
get createdAt() {
return new Date(this.#createdAt); // 복사본 반환 (불변성 보장)
}
// 읽기/쓰기 가능 (getter + setter)
get name() {
return this.#name;
}
set name(value) {
if (!value || value.trim() === '') {
throw new Error('Name cannot be empty');
}
this.#name = value.trim();
}
}
const user = new User('Alice');
console.log(user.id); // "abc123"
console.log(user.name); // "Alice"
user.name = 'Bob'; // ✅ 가능
user.id = 'new-id'; // ⚠️ 무시됨 (setter 없음)
console.log(user.id); // 여전히 "abc123"
5. 상속 시 Private 필드 동작
Private 필드는 상속되지 않으며, 자식 클래스에서 접근할 수 없습니다.
class Parent {
#privateField = 'parent secret';
_protectedField = 'parent protected';
publicField = 'parent public';
#privateMethod() {
return 'private method';
}
getPrivate() {
return this.#privateField;
}
callPrivateMethod() {
return this.#privateMethod();
}
}
class Child extends Parent {
#privateField = 'child secret'; // ✅ 다른 필드로 취급됨
testAccess() {
console.log(this.publicField); // ✅ "parent public"
console.log(this._protectedField); // ✅ "parent protected"
// console.log(this.#privateField); // ⚠️ "child secret" (부모 것 아님!)
console.log(super.getPrivate()); // ✅ "parent secret" (메서드 통해 접근)
// this.#privateMethod(); // ❌ SyntaxError
console.log(super.callPrivateMethod()); // ✅ "private method"
}
getChildPrivate() {
return this.#privateField; // "child secret"
}
}
const child = new Child();
child.testAccess();
console.log(child.getChildPrivate()); // "child secret"
console.log(child.getPrivate()); // "parent secret"
TypeScript의 protected vs JavaScript의 private
class Parent {
private privateField = 'private'; // 자식 접근 불가
protected protectedField = 'protected'; // 자식 접근 가능
public publicField = 'public'; // 모두 접근 가능
}
class Child extends Parent {
test() {
// console.log(this.privateField); // ❌ 컴파일 에러
console.log(this.protectedField); // ✅ 가능
console.log(this.publicField); // ✅ 가능
}
}
6. Private 필드와 성능 고려사항
일반 프로퍼티 vs Private 필드
class RegularClass {
publicField = 'public';
getValue() {
return this.publicField;
}
}
class PrivateClass {
#privateField = 'private';
getValue() {
return this.#privateField;
}
}
// 성능 테스트
console.time('Regular');
for (let i = 0; i < 1000000; i++) {
const obj = new RegularClass();
obj.getValue();
}
console.timeEnd('Regular'); // ~50ms
console.time('Private');
for (let i = 0; i < 1000000; i++) {
const obj = new PrivateClass();
obj.getValue();
}
console.timeEnd('Private'); // ~52ms (거의 동일)
성능 차이:
- Modern JS 엔진 (V8, SpiderMonkey)은 private 필드를 최적화함
- 실제 성능 차이는 미미함 (대부분 경우 무시 가능)
- 보안성과 캡슐화가 훨씬 중요
주의사항:
class Counter {
#count = 0;
// ✅ 좋음 - 단순 접근
increment() {
this.#count++;
}
// ⚠️ 피하기 - 불필요한 반복 접근
badIncrement() {
for (let i = 0; i < 1000; i++) {
this.#count = this.#count + 1; // 반복적인 lookup
}
}
// ✅ 더 나음 - 로컬 변수 활용
goodIncrement() {
let count = this.#count;
for (let i = 0; i < 1000; i++) {
count++;
}
this.#count = count;
}
}
7. Private 필드 디버깅
Chrome DevTools에서 private 필드 확인
class Debug {
#secret = 'hidden';
public = 'visible';
#privateMethod() {
return 'secret function';
}
reveal() {
debugger; // 여기서 멈춤
return this.#secret;
}
}
const obj = new Debug();
obj.reveal();
// DevTools Console:
// > obj
// Debug {public: 'visible', #secret: 'hidden'}
//
// > obj.#secret // ❌ SyntaxError
// > obj.public // ✅ "visible"
Console에서 private 접근 (비추천)
class MyClass {
#data = 'secret';
getData() {
return this.#data;
}
}
const instance = new MyClass();
// 방법 1: 메서드 통해 접근
console.log(instance.getData()); // "secret"
// 방법 2: DevTools에서만 가능
// Chrome DevTools > Sources > Scope 섹션에서 확인 가능
8. Private 필드와 JSON 직렬화
Private 필드는 JSON.stringify()에서 자동으로 제외됩니다.
class User {
#password;
#ssn;
username;
email;
constructor(username, email, password, ssn) {
this.username = username;
this.email = email;
this.#password = password;
this.#ssn = ssn;
}
// 자동 JSON 직렬화
toJSON() {
return {
username: this.username,
email: this.email
// private 필드는 의도적으로 제외
};
}
// password 확인용 메서드
verifyPassword(input) {
return this.#password === input;
}
}
const user = new User('alice', 'alice@example.com', 'secret123', '123-45-6789');
// ✅ Private 필드 자동 제외
console.log(JSON.stringify(user));
// {"username":"alice","email":"alice@example.com"}
// ❌ 직접 접근 불가
console.log(user.#password); // SyntaxError
console.log(user.#ssn); // SyntaxError
// ✅ 메서드로 검증만 가능
console.log(user.verifyPassword('secret123')); // true
WeakMap과 JSON 직렬화
const privateData = new WeakMap();
class Product {
constructor(name, price, cost) {
this.name = name;
this.price = price;
// cost는 private (이윤 계산용, 외부 노출 금지)
privateData.set(this, { cost });
}
getProfit() {
const { cost } = privateData.get(this);
return this.price - cost;
}
toJSON() {
return {
name: this.name,
price: this.price
// cost는 직렬화 안됨
};
}
}
const product = new Product('Laptop', 1000, 600);
console.log(JSON.stringify(product));
// {"name":"Laptop","price":1000}
console.log(product.getProfit()); // 400
9. 프레임워크별 Private 컨벤션
React 컴포넌트
class Counter extends React.Component {
// ✅ Public state
state = { count: 0 };
// ✅ Private 헬퍼 메서드
#calculateNextValue(delta) {
return Math.max(0, this.state.count + delta);
}
// ✅ Public 이벤트 핸들러 (외부에서 ref로 호출 가능)
increment = () => {
const nextValue = this.#calculateNextValue(1);
this.setState({ count: nextValue });
};
// ✅ Public 라이프사이클
componentDidMount() {
console.log('Component mounted');
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>+1</button>
</div>
);
}
}
Vue 3 Composition API
import { ref, computed } from 'vue';
export default {
setup() {
// ✅ Private (setup 내부에서만 접근)
const _internalState = ref(0);
function _validateInput(value) {
return value >= 0 && value <= 100;
}
// ✅ Public (template과 외부에 노출)
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
if (_validateInput(count.value + 1)) {
count.value++;
}
}
// Public만 반환
return {
count,
doubleCount,
increment
};
}
};
Angular 서비스
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
// ✅ Private (Angular DI에서만 접근)
private cache = new Map<string, any>();
private validateKey(key: string): boolean {
return key.length > 0;
}
// ✅ Public (컴포넌트에서 사용)
public getData(key: string): any {
if (!this.validateKey(key)) {
throw new Error('Invalid key');
}
return this.cache.get(key);
}
public setData(key: string, value: any): void {
if (!this.validateKey(key)) {
throw new Error('Invalid key');
}
this.cache.set(key, value);
}
}
10. JavaScript 엔진별 Private 필드 최적화
V8 (Chrome, Node.js)
// V8은 private 필드를 "hidden class"로 최적화
class Optimized {
#x;
#y;
constructor(x, y) {
this.#x = x; // 인라인 캐싱 가능
this.#y = y;
}
sum() {
return this.#x + this.#y; // JIT 최적화됨
}
}
브라우저 지원 현황 (2024년 기준)
| 엔진 | # Private |
Static Block | 버전 |
|---|---|---|---|
| V8 (Chrome) | ✅ | ✅ | 74+ / 94+ |
| SpiderMonkey (Firefox) | ✅ | ✅ | 90+ / 93+ |
| JavaScriptCore (Safari) | ✅ | ✅ | 14.1+ / 16.4+ |
| Node.js | ✅ | ✅ | 12+ / 16.11+ |
11. 패턴: Private + Proxy
class SecureStore {
#data = new Map();
#accessLog = [];
#logAccess(action, key) {
this.#accessLog.push({
action,
key,
timestamp: Date.now()
});
}
set(key, value) {
this.#logAccess('set', key);
this.#data.set(key, value);
}
get(key) {
this.#logAccess('get', key);
return this.#data.get(key);
}
getAccessLog() {
return [...this.#accessLog]; // 복사본 반환
}
}
// Proxy로 래핑하여 추가 검증
function createSecureStore() {
const store = new SecureStore();
return new Proxy(store, {
get(target, prop) {
// 특정 메서드만 노출
if (['set', 'get', 'getAccessLog'].includes(prop)) {
return target[prop].bind(target);
}
return undefined;
},
set() {
throw new Error('Cannot modify store directly');
}
});
}
const store = createSecureStore();
store.set('key1', 'value1');
console.log(store.get('key1')); // "value1"
console.log(store.getAccessLog());
// [{ action: 'set', key: 'key1', ... }, { action: 'get', key: 'key1', ... }]
// ❌ 직접 수정 불가
store.newProp = 'test'; // Error: Cannot modify store directly
참조
- MDN: Private class features
- TypeScript: Classes
- JavaScript.info: Private and protected properties and methods
마이그레이션 예시
Before (Legacy)
class OldClass {
static _helper() {
return 'private';
}
static create() {
return this._helper();
}
}
After (Modern)
class NewClass {
static #helper() {
return 'private';
}
static create() {
return this.#helper();
}
}
After (TypeScript)
class NewClass {
private static helper(): string {
return 'private';
}
public static create(): string {
return this.helper();
}
}
댓글