이전 포스팅 https://ipjaworld.tistory.com/63 에서 이어지는 내용입니다.
포스팅은 3편에 나눠서 업로드 됩니다.
A. 자바스크립트 조망하기
1. 강제변환과 깊은비교, 일치비교와 동등비교,
2. js의 this 키워드와 프로토타입, 그리고 메모리주소
B. 추가 주제
1. es6 모듈에서 class를 사용하지 않는 이유
B. 추가 주제
1. ES6 모듈에서 class를 사용하지 않는 이유
ES6(ECMAScript 2015)에서는 클래스 문법과 모듈 시스템이 동시에 도입되었다. 그러나 "You Don't Know JS Yet"을 읽으면서 흥미로웠던 점은 많은 자바스크립트 개발자들, 특히 함수형 프로그래밍을 선호하는 개발자들이 ES6 모듈에서 클래스 사용을 지양한다는 것이다. 왜 그럴까?
클래스의 겉모습과 실제 동작의 차이
자바스크립트의 클래스는 다른 언어(Java, C++ 등)의 클래스와 문법적으로 유사하지만, 내부적으로는 여전히 프로토타입 기반으로 동작한다. 이런 "문법적 설탕(syntactic sugar)"은 자바스크립트의 객체 지향 특성을 이해하지 못한 채 다른 언어의 패러다임을 그대로 가져오게 만든다.
// 클래스 문법
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
// 위 코드는 내부적으로 다음과 같이 동작한다
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
this 바인딩 문제
클래스 메서드를 이벤트 핸들러나 콜백으로 전달할 때 this 바인딩 문제가 자주 발생한다.
class Counter {
constructor() {
this.count = 0;
this.button = document.getElementById('button');
// this 바인딩을 수동으로 처리해야 함
this.button.addEventListener('click', this.increment.bind(this));
}
increment() {
this.count++;
console.log(this.count);
}
}
이런 코드는 this 바인딩을 수동으로 처리해야 하므로 실수하기 쉽고, 코드의 복잡성이 증가한다.
상태 관리와 불변성
함수형 프로그래밍에서는 상태 변경을 최소화하고 불변성(immutability)을 중요시한다. 클래스는 본질적으로 내부 상태를 가지며, 메서드를 통해 그 상태를 변경하는 방식으로 동작한다.
// 클래스 기반 접근법 (상태 변경)
class Cart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item); // 내부 상태 변경
}
getTotal() {
return this.items.reduce((total, item) => total + item.price, 0);
}
}
// 함수형 접근법 (불변성 유지)
const createCart = (items = []) => ({
items,
addItem: (item) => createCart([...items, item]), // 새 객체 반환
getTotal: () => items.reduce((total, item) => total + item.price, 0)
});
함수형 접근법은 원본 데이터를 변경하지 않고 새로운 상태를 반환하므로, 예측 가능성이 높아지고 부작용(side effects)이 감소한다.
모듈 패턴의 유연성
ES6 모듈은 그 자체로 격리된 스코프를 제공하므로, 클래스 없이도 캡슐화를 쉽게 구현할 수 있다.
// counter.js
let count = 0; // 모듈 내부에서만 접근 가능한 private 상태
export const increment = () => ++count;
export const decrement = () => --count;
export const getCount = () => count;
// 사용
import { increment, getCount } from './counter.js';
increment();
console.log(getCount()); // 1
이 패턴은 클래스보다 더 간결하고, 필요한 기능만 노출할 수 있어 API 설계가 명확해진다.
상속보다 합성
클래스 상속은 종종 "깨지기 쉬운 기반 클래스(fragile base class)" 문제를 일으킨다. 부모 클래스의 변경이 예기치 않게 자식 클래스에 영향을 미칠 수 있기 때문이다.
// 상속 기반 접근법
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks.`);
}
}
// 합성 기반 접근법
const createAnimal = (name) => ({
name,
speak: () => console.log(`${name} makes a noise.`)
});
const createDog = (name) => ({
...createAnimal(name),
speak: () => console.log(`${name} barks.`)
});
합성(composition)은 "has-a" 관계를 표현하며, 상속보다 더 유연하고 느슨한 결합을 제공한다.
테스트 용이성
함수형 접근법은 일반적으로 테스트하기 더 쉽다. 순수 함수(pure functions)는 동일한 입력에 대해 항상 동일한 출력을 반환하며, 외부 상태에 의존하지 않는다.
// 클래스 메서드 테스트 (상태에 의존)
test('Cart.addItem', () => {
const cart = new Cart();
cart.addItem({ name: 'Product', price: 10 });
expect(cart.items.length).toBe(1);
});
// 함수형 접근법 테스트 (상태 독립적)
test('addItem', () => {
const cart = createCart();
const newCart = cart.addItem({ name: 'Product', price: 10 });
expect(newCart.items.length).toBe(1);
expect(cart.items.length).toBe(0); // 원본은 변경되지 않음
});
클래스 vs 함수형 패턴 비교표
아래 표는 클래스 기반 접근법과 함수형 패턴의 주요 차이점을 비교한 것이다:
특성 클래스 기반 접근법 함수형 패턴
상태 관리 | 내부 상태를 변경 (mutable) | 불변 상태 (immutable) 지향 |
코드 구조 | 객체와 메서드 중심 | 함수와 데이터 중심 |
상속 방식 | 클래스 상속 (is-a 관계) | 객체 합성 (has-a 관계) |
this 처리 | 컨텍스트 관리 필요 | 컨텍스트 의존성 최소화 |
부작용 | 객체 내부 상태 변경 흔함 | 부작용 최소화 지향 |
테스트 용이성 | 상태 의존적, 복잡할 수 있음 | 순수 함수로 테스트 용이 |
메모리 사용 | 인스턴스마다 메모리 할당 | 함수 공유, 효율적 메모리 사용 가능 |
디버깅 | 상태 추적이 어려울 수 있음 | 데이터 흐름 추적 용이 |
확장성 | 상속을 통한 확장 | 합성을 통한 확장 |
가독성 | 객체지향 개발자에게 친숙 | 함수형 패러다임 이해 필요 |
실제 프레임워크 사례: React의 함수형 컴포넌트 전환
React는 초기에 클래스 컴포넌트를 주로 사용했지만, 16.8 버전에서 Hooks가 도입되면서 함수형 컴포넌트로의 전환이 가속화되었다. 이는 프론트엔드 개발에서 함수형 접근법의 장점을 잘 보여주는 사례다.
// 클래스 컴포넌트
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}
increment() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
// 함수형 컴포넌트 + Hooks
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
함수형 컴포넌트는 더 간결하고, this 바인딩 문제가 없으며, Hooks를 통해 상태 관리와 생명주기 기능을 필요한 만큼만 사용할 수 있다. React 팀은 공식적으로 함수형 컴포넌트와 Hooks 사용을 권장하고 있으며, 이는 자바스크립트 생태계에서 함수형 프로그래밍의 영향력을 보여준다.
또한 React의 state 업데이트 철학은 불변성에 기반하며, 이는 함수형 프로그래밍의 핵심 원칙과 일치한다:
// 권장되지 않는 방식 (직접 상태 변경)
const handleClick = () => {
const newItems = this.state.items;
newItems.push('New Item');
this.setState({ items: newItems }); // 잘못된 패턴
};
// 권장되는 방식 (불변성 유지)
const handleClick = () => {
this.setState(prevState => ({
items: [...prevState.items, 'New Item'] // 새 배열 생성
}));
};
다른 라이브러리와 프레임워크에서의 함수형 접근
React 외에도 많은 현대적인 자바스크립트 라이브러리와 프레임워크들이 함수형 접근법을 채택하고 있다:
- Redux: 예측 가능한 상태 관리를 위해 순수 함수인 리듀서(reducer)와 불변성 원칙을 따른다.
- Vue 3: Composition API를 도입하여 함수형 접근법을 지원한다.
- Svelte: 선언적 템플릿과 반응형 상태를 사용하며, 클래스 대신 함수형 패러다임을 채택한다.
- Functional CSS Libraries (Tailwind, Tachyons): 작은 단일 기능 클래스를 합성하여 스타일을 구성하는 방식은 합성 개념과 일치한다.
성능 고려사항
클래스 인스턴스 생성은 프로토타입 체인 설정과 new 연산자 오버헤드가 있다. 단순 객체나 클로저를 사용하는 팩토리 함수는 경우에 따라 더 효율적일 수 있다.
물론 현대 자바스크립트 엔진은 최적화가 잘 되어 있어 대부분의 경우 성능 차이는 미미하지만, 성능이 중요한 애플리케이션에서는 고려할 만한 요소다.
결론: 상황에 맞는 도구 선택하기
클래스가 항상 나쁜 것은 아니다. 특히 다음과 같은 경우에는 클래스가 적합할 수 있다:
- 객체 지향 설계가 자연스러운 도메인(예: UI 컴포넌트)
- 상속이 명확하게 "is-a" 관계를 표현할 때
- 팀이 클래스 기반 패러다임에 더 익숙한 경우
- 기존 클래스 기반 라이브러리/프레임워크와 통합해야 할 경우
하지만 모듈 시스템을 사용할 때는 항상 가장 단순하고 명확한 방법을 선택하는 것이 좋다. 대부분의 경우, 간단한 팩토리 함수나 객체 리터럴, 또는 순수 함수의 집합으로도 충분하다.
"You Don't Know JS Yet"에서 배운 가장 중요한 것 중 하나는 자바스크립트의 유연성을 이해하고, 특정 패러다임에 얽매이지 않고 상황에 가장 적합한 도구를 선택하는 것이다. 클래스가 필요하다면 사용하되, 단순히 다른 언어의 패턴을 모방하기 위해 사용하는 것은 피하자.
'잡담' 카테고리의 다른 글
You Don't Know JS Yet을 읽고: js의 this 키워드와 프로토타입, 그리고 메모리주소 (0) | 2025.03.02 |
---|---|
You Don't Know JS Yet을 읽고: 자바스크립트 타입 비교의 오해와 진실 (0) | 2025.03.02 |
프로젝트 자가진단 체크리스트 (2) | 2023.02.03 |