PUBLISHED
자바스크립트에서의 멀티패러다임 프로그래밍
작성일: 2025.03.15

이 글의 제목은 ‘자바스크립트에서의 멀티패러다임 프로그래밍’이지만, 우선은 영어에 대해 이야기해보자. 다소 뜬금없다고 느낄 수도 있겠지만, 다 이유가 있다.
아주 오래전, 국제 외교의 언어는 프랑스어였다. 왕실 간의 외교문서와 고위 외교관의 대화는 프랑스어로 쓰이는 것이 관례였다. 하지만 지금은 전 세계 어디서든 영어가 표준 언어처럼 쓰인다. 그 배경에는 분명히 영국의 제국주의와 미국의 군사력, 경제력, 그리고 인터넷과 미디어 산업의 힘이 있다.
하지만 나는 거기에 더해 영어 자체의 문법적 유연함도 중요한 역할을 했다고 생각한다. 영어는 문법이 다소 틀려도 의미가 통하고, 억양이나 맥락으로도 충분히 의사소통이 가능하다. 완벽하지 않아도 전달되는 언어, 이것이 영어가 세계적으로 퍼질 수 있었던 이유 중 하나였을지도 모른다.
JavaScript도 비슷한 부분이 있다. HTML 안에 스크립트로 삽입할 수 있을 정도로 가볍고, 처음 접하는 사람도 일단 돌아가는 코드를 쉽게 만들 수 있다. 엄격한 타입 체크나 컴파일 과정 없이도 작동한다. 오류가 나도 어찌어찌 실행이 되기도 하고, 함수가 값이 되고, 값이 함수가 되기도 한다. 정형화된 문법보다는 실용과 관용을 중시하는 철학이 깔려 있는 언어다. 이 느슨함은 단점이 되기도 하지만, 동시에 진입 장벽을 낮추고 실험을 가능하게 하는 장점이 되기도 한다.
특히 JavaScript는 여러 가지 프로그래밍 패러다임을 허용한다는 점에서 독특하다. 객체지향처럼 class를 선언해도 되고, 함수형 스타일로 map, filter, reduce를 조합해도 된다. 필요한 경우 절차형 루프를 써도 문제없다. JavaScript는 다양한 스타일을 강요하지 않고 받아들인다. 이 유연성이 JavaScript를 멀티패러다임 언어로 만든다. 그리고 이 멀티패러다임성은, 우리가 문제를 어떻게 바라보고 어떻게 풀 것인가를 더 깊이 고민하게 만드는 기회를 제공한다.
멀티패러다임 언어
멀티패러다임 언어란, 하나의 언어 안에서 여러 가지 프로그래밍 스타일(패러다임)을 선택적으로 사용할 수 있는 언어를 말한다. 패러다임은 일종의 사고방식이며, 같은 문제를 전혀 다른 방식으로 접근할 수 있게 해준다. 대표적으로는 객체지향(Object-Oriented), 함수형(Functional), 절차형(Procedural) 프로그래밍이 있다. 이 셋은 서로 다른 철학과 설계 방식 위에 서 있지만, JavaScript는 이들을 강요하지 않고 모두 포용한다.
예를 들어, 어떤 개발자는 class와 this를 써서 Java처럼 객체지향 스타일로 코드를 작성하고, 다른 개발자는 상태를 바꾸지 않는 순수 함수로 데이터를 변환하는 함수형 스타일을 쓴다. 또 어떤 날은 단순하게 for 루프를 돌려서 문제를 해결하기도 한다. JavaScript는 이 세 가지 스타일을 모두 자연스럽게 구현할 수 있는 언어이고, 이 유연성은 학습자에게는 다양한 시도를 가능하게 하고, 실무자에게는 상황에 맞는 전략적 선택지를 제공한다.
객체지향 vs 함수형: 코드 분해 방식의 차이
각 패러다임은 코드를 어떻게 분해하고 구성할 것인가에 대한 철학이 다르다. 객체지향 프로그래밍(OOP)은 코드를 정보(상태)와 행동(메서드)으로 구분한다. 어떤 객체가 있고, 그 객체는 내부 상태를 가지며, 그 상태를 바꾸거나 조회하는 메서드를 통해 외부와 상호작용한다. 예를 들어 User라는 객체가 있고, 그 객체는 name과 email 같은 정보를 가지고 있으며, 이를 수정하는 updateEmail 같은 메서드를 가진다.
// 객체지향 스타일
class Counter {
private count = 0;
increment() {
this.count += 1;
}
getCount() {
return this.count;
}
}
const counter = new Counter();
counter.increment();
console.log(counter.getCount()); // 1반면 함수형 프로그래밍(FP)은 코드를 정보(값)와 함수, 그중에서도 순수 함수(pure function)와 부수 효과(side-effect)를 일으키는 함수로 나눈다. 함수형의 핵심은 불변성과 함수의 참조 투명성이다. 어떤 입력이 주어졌을 때 항상 같은 출력을 반환하고, 외부 상태를 변경하지 않는 함수가 선호된다. 상태 변경이 필요한 경우에도, 기존 데이터를 직접 수정하는 대신 새로운 값을 반환하는 방식으로 처리한다.
// 함수형 스타일
type CounterState = { count: number };
const increment = (state: CounterState): CounterState => ({
count: state.count + 1,
});
const getCount = (state: CounterState): number => state.count;
const initialState = { count: 0 };
const nextState = increment(initialState);
console.log(getCount(nextState)); // 1즉, 객체지향은 행동을 메서드로 구현하고, 함수형은 그것을 순수 함수로 모델링하려 한다. 흥미롭게도, 객체지향의 메서드도 결국은 함수다. 그렇다면 이 함수들이 순수하다면, 객체지향적인 구조를 함수형적으로 작성할 수 있는 것 아닐까?
함수는 순수할 수도 있고 아닐 수도 있다
객체지향에서 메서드는 결국 함수다. 함수라는 점에서는 객체지향과 함수형 사이에 실질적인 경계가 없다. 차이는 그 함수가 상태를 바꾸는가, 그렇지 않은가다.
순수 함수: 외부 상태에 의존하지 않고, 부수 효과가 없다.
부수 효과 함수: 로그를 남기거나, DOM을 조작하거나, 객체 내부 상태를 변경한다.
예를 들어 아래는 동일한 기능을 하는 두 메서드이지만, 함수형 관점에서 보면 전혀 다르게 평가된다. 첫 번째 방식은 전통적인 객체지향 스타일이다. 객체 내부 상태를 직접 바꾸며 시간의 흐름을 표현한다. 반면 두 번째 방식은 불변성과 순수 함수의 원칙을 따르는 객체지향이다. tick은 내부 상태를 바꾸지 않고, 새로운 객체를 반환한다. 이런 방식은 함수형 사고로 객체지향 구조를 다시 설계한 좋은 예라고 할 수 있다.
// 부수 효과가 있는 메서드 (불순)
class Timer1 {
private seconds = 0;
tick() {
this.seconds += 1;
}
getSeconds() {
return this.seconds;
}
}
// 순수한 메서드처럼 구현 (함수형적)
class Timer2 {
constructor(private readonly seconds: number) {}
tick(): Timer2 {
return new Timer2(this.seconds + 1);
}
getSeconds() {
return this.seconds;
}
}
const t1 = new Timer2(0);
const t2 = t1.tick();
console.log(t1.getSeconds()); // 0
console.log(t2.getSeconds()); // 1함수형적 객체지향
객체지향 프로그래밍(OOP)은 상태와 행동을 하나의 객체에 묶어 관리한다. 이 접근법은 코드의 구조를 직관적으로 만들고, 현실 세계의 개념을 모델링하는 데 강력하다. 하지만 전통적인 객체지향에서 메서드는 종종 객체의 내부 상태를 직접 변경하는 부수 효과를 동반한다. 이는 복잡도가 증가할 때 테스트와 유지보수를 어렵게 만든다.
함수형 프로그래밍(FP)은 이러한 문제에 대한 해법을 제공한다. FP는 상태 변경을 피하고, 상태를 불변으로 유지하며, 함수는 입력에만 의존하고 외부 상태에 영향을 끼치지 않는 순수 함수가 되도록 강조한다. 그런데 이 두 패러다임은 서로 배타적이지 않다. 우리는 객체지향의 구조적 이점을 유지하면서도 함수형 프로그래밍의 순수성과 불변성을 결합하는, 즉 함수형적 객체지향을 구현할 수 있다.
객체지향 스타일에서 사용자 목록을 필터링하고 가공하는 복잡한 메서드를 예로 들어보자. 아래의 메서드는 객체 상태를 직접 읽고, 여러 단계를 내부에서 처리한다. 동작은 명확하지만, 각 단계가 묶여 있어 개별 테스트가 어렵고, 함수 단위 재사용도 제한적이다.
class UserProcessor {
constructor(private users: User[]) {}
process() {
const activeUsers = this.users.filter(user => user.isActive);
const adults = activeUsers.filter(user => user.age >= 18);
const upperCaseNames = adults.map(user => user.name.toUpperCase());
const filteredNames = upperCaseNames.filter(name => name.length >= 5);
return filteredNames;
}
}이러한 문제를 해결하기 위해, 함수형 프로그래밍을 지원하는 라이브러리 FxTS를 활용해 사용자 목록을 함수형 스타일로 처리할 수 있다. FxTS는 pipe, filter, map 같은 함수 합성 도구를 제공해, 데이터를 여러 단계로 나누어 순차적으로 처리할 수 있도록 돕는다. 이를 통해 코드의 가독성과 재사용성을 높이고, 부수 효과 없이 순수 함수 중심으로 깔끔하게 문제를 해결할 수 있다.
import { pipe, filter, map, toArray } from "@fxts/core"
class UserProcessor {
constructor(private users: User[]) {}
public processUsers(): string[] {
return pipe(
this.users,
filter(user => user.isActive),
filter(user => user.age >= 18),
map(user => user.name.toUpperCase()),
filter(name => name.length >= 5),
toArray
)
}
}보다 명확한 역할 분리를 위해 각각의 함수를 아래와 같이 단일 메서드로 분리할 수 있다. 이렇게 하면 각 단계별로 독립적인 단위 테스트를 작성하기가 훨씬 수월해지고, 특정 조건에 맞는 필터링이나 변환 로직을 개별적으로 검증할 수 있어 코드의 안정성과 유지보수성이 크게 향상된다.
class UserProcessor {
constructor(private users: User[]) {}
private filterActiveUsers = filter(user => user.isActive);
private filterAdultUsers = filter(user => user.age >= 18);
private mapNamesToUpperCase = map(user => user.name.toUpperCase());
private filterLongNames = filter((name: string) => name.length >= 5);
public processUsers(): string[] {
return pipe(
this.users,
this.filterActiveUsers,
this.filterAdultUsers,
this.mapNamesToUpperCase,
this.filterLongNames,
toArray
);
}
}간단한 결론
나는 평소 객체지향 프로그래밍에 익숙하고 익숙해져 있어서, 이런 스타일을 멀티패러다임 프로그래밍이라기보다는 함수형 개념을 장착한 객체지향 프로그래밍에 가깝다고 느낀다. 객체와 그 안의 상태, 행동을 중심으로 사고하는 습관이 워낙 깊게 배어 있기 때문이다. 하지만 결국 중요한 건 ‘어떤 이름을 붙이느냐’가 아니라 ‘어떻게 문제를 효과적으로 해결하느냐’라고 생각한다. 객체지향의 명확한 구조와 캡슐화를 통해 복잡한 상태를 효과적으로 관리하고, 때로는 함수형의 순수 함수와 함수 합성을 통해 코드의 예측 가능성과 재사용성을 높인다.
이러한 유연성 덕분에 개발자는 각 문제 상황에 가장 적합한 프로그래밍 패러다임을 선택할 수 있으며, 객체지향의 명확한 구조와 함수형의 순수성, 재사용성을 조화롭게 활용할 수 있다. 결과적으로 더 견고하고 확장 가능한 코드를 작성할 수 있을 뿐만 아니라, 유지보수가 쉬워져 장기적인 프로젝트 품질 향상에도 큰 도움이 된다. 멀티패러다임 프로그래밍은 단순히 여러 스타일을 혼용하는 것이 아니라, 각 패러다임의 강점을 이해하고 적절히 결합하여 문제 해결의 폭과 깊이를 넓히는 ‘실용적인 사고방식’이라 할 수 있다.