홈으로 돌아가기

JS/TS는 함수형 언어가 아닙니다: 기술 분석

이 기사는 JavaScript와 TypeScript가 함수형 프로그래밍 언어가 아닌 이유를 설명합니다. 네 가지 주요 기술적 제한 사항을 검토합니다: 기본 가변성, 꼬리 재귀 최적화 부족, 지연 컬렉션 및 구조적 패턴 매칭 부족, 그리고 예외를 통한 오류 처리. 예제와 개발에 미치는 영향이 제공됩니다.

JS/TS는 함수형 언어가 아닙니다: 기술적 제한이 코드에 미치는 영향
Advertisement 728x90

JS/TS는 함수형 언어가 아니다: 기술적 한계와 그 결과

많은 개발자들이 .map()이나 .filter() 같은 메서드 때문에 JavaScript와 TypeScript를 함수형 언어로 착각합니다. 하지만 이 언어들은 함수형 프로그래밍의 핵심 원칙을 지원하지 않아 프로덕션 코드에 숨겨진 버그가 발생합니다. JS/TS가 함수형 언어가 아닌 4가지 기술적 이유와 개발에 미치는 영향을 살펴보겠습니다.

기본 가변성: 불변성의 착각

Haskell이나 Clojure 같은 함수형 언어에서는 불변성이 언어에 내장되어 있습니다. 예를 들어 Clojure에서는 기본 데이터 구조가 기본적으로 불변입니다. 반면 JavaScript에서는 가변성이 기본 동작입니다. 심지어 const로 선언해도 불변성을 보장하지 않습니다:

const user = { name: "Alice" };
user.name = "Bob"; // 오류 없이 동작

여기서 const는 변수 재할당만 막을 뿐 객체 내용의 변경은 허용합니다. 이는 참조 투명성(referential transparency)을 깨뜨립니다. 참조 투명성은 표현식을 그 값으로 대체해도 프로그램 동작이 변하지 않는 함수형 프로그래밍의 핵심 속성입니다.

Google AdInline article slot

Object.freeze()나 Immutable.js 같은 라이브러리로 불변성을 흉내내려는 시도는 임시방편일 뿐입니다. 언어에 내장되지 않아 팀 차원의 명시적 결정이 필요하고, 채택이 제한적입니다. 이런 설계 이유는 역사적입니다: JavaScript는 1995년에 10일 만에 브라우저용 간단한 스크립팅 언어로 만들어졌고, 가변성은 구현을 단순화하고 C/Java 프로그래머들의 습관에 맞췄습니다. 오늘날 웹을 망가뜨리지 않고 메모리 모델을 재설계하는 건 불가능합니다.

꼬리 호출 최적화 부재: 동적성의 대가

ES2015 표준에서 제안된 꼬리 호출 최적화(TCO)는 Safari를 제외한 주요 엔진에서 여전히 지원되지 않습니다. Chrome과 Firefox에서는 깊은 재귀가 스택 오버플로를 일으킵니다:

function factorial(n, acc = 1) {
  if (n <= 1) return acc;
  return factorial(n - 1, n * acc);
}
factorial(100000); // RangeError: Maximum call stack size exceeded

Scala에서는 @tailrec 어노테이션을 붙이면 컴파일러가 반복문으로 최적화해 스택 오버플로를 피합니다. 컴파일 타임에 최적화 가능성을 검사해 꼬리 호출이 불가능하면 오류를 냅니다.

Google AdInline article slot

V8이 TCO를 지원하지 않는 이유는 다음과 같습니다:

  • 스택 트레이스 손실. TCO는 현재 스택 프레임을 대체해 디버깅을 어렵게 합니다. Scala 컴파일러는 재귀를 반복문으로 바꾸면서도 호출 스택을 보존합니다.
  • 레거시 API와의 하위 호환성. Function.caller 같은 메서드는 호출 이력에 의존하는데 TCO가 이를 지웁니다.
  • JIT 컴파일 복잡성. V8의 다단계 아키텍처(Ignition → TurboFan)에서 프레임 생성과 무효화를 재설계해야 합니다.

V8 엔지니어들은 TCO 구현 비용이 생태계 이익을 초과한다고 공개적으로 밝혔습니다. 이는 JS의 우선순위—동적 기능과 하위 호환성—를 반영합니다. 엄격한 함수형 보장은 뒷전입니다.

지연 컬렉션과 구조적 공유: 빅데이터에 부적합

JavaScript에서 배열 연산(filter, map)은 중간 복사본을 만들어 메모리 사용량을 폭증시킵니다:

Google AdInline article slot
const result = hugeArray
  .filter(x => x > 0)   // [1] 배열 A 생성
  .map(x => x * 2)      // [2] 배열 B 생성
  .filter(x => x < 100) // [3] 배열 C 생성
  .slice(0, 10);        // [4] 결과 생성

이 모든 게 메모리에 동시에 존재할 수 있습니다—hugeArray, A, B, C—최종 결과가 작아도요. 가비지 컬렉터는 비동기라 피크 로드가 현실적입니다.

Scala에서는 .view로 지연 뷰를 만들어 중간 컬렉션 없이 온디맨드 파이프라인으로 만듭니다. 게다가 Vector 같은 영속 구조는 구조적 공유를 사용해 "변경" 시 루트에서 리프까지 경로만 복사(O(log n))하고 나머지는 재사용합니다.

JS는 이를 네이티브로 지원하지 않습니다. 제너레이터와 ES2025의 새로운 Iterator Helpers로 지연 체인을 만들 수 있지만, 명령형 코드가 필요하고 표준 API가 아닙니다. 이유는 주류 사용 사례 최적화입니다:

  • 데이터 지역성. 연속 배열은 CPU 캐시와 잘 맞지만 지연 체인은 작은 호출을 유발합니다.
  • JIT 최적화. V8은 Array.prototype 메서드를 공격적으로 인라인하지만 지연 연산은 막습니다.
  • UI 성능. 인터페이스 렌더링에서 배열 복사가 트리 포인터 추적보다 빠를 수 있습니다.

오류 처리: 예외 vs. 타입화된 결과

JavaScript에서 오류는 타입 시스템에 숨겨진 예외입니다. 예를 들어 JSON.parse의 시그니처는 (string) => any지만 예외를 던질 수 있습니다:

function getUser(id: string): User {
  // 예외를 던질 수 있음—컴파일러가 경고 안 함
}

예외는 참조 투명성을 깨고 전체 함수를 부분 함수로 만듭니다. 함수형 언어는 반환 타입에 오류를 인코딩합니다:

val result: Try[Json] = Try(parse(userInput))

여기서 Try는 잠재적 실패를 명시하고 컴파일러가 둘 다 처리하도록 요구합니다. JS에서 try/catch는 문(statement)이라 데이터 흐름을 끊고 컴포지션을 복잡하게 합니다.

fp-ts 같은 라이브러리가 임시방편을 제공하지만 외부 솔루션일 뿐 언어 내장 기능이 아닙니다. 이로 인해 타입 시스템에 맹점이 생겨 오류가 예기치 않게 발생합니다.

개발자를 위한 핵심 요약

  • JS/TS는 불변성을 보장하지 않습니다. const조차 객체 변경을 막지 못합니다. 진짜 불변성은 Immutable.js 같은 워크어라운드가 필요해 코드 복잡도가 증가합니다.
  • JS에서 재귀는 위험합니다. TCO 부재로 재귀 알고리즘은 빅데이터에 부적합합니다. 항상 재귀 깊이를 확인하세요.
  • 지연 연산은 네이티브가 아닙니다. 메모리 스파이크 없이 대형 배열을 다루려면 명령형 코드나 서드파티 라이브러리가 필요합니다.
  • 오류는 숨겨진 위험입니다. 함수 시그니처가 예외를 반영하지 않습니다. TS가 요구하지 않아도 신뢰할 수 없는 연산은 항상 try/catch로 감싸세요.

— Editorial Team

Advertisement 728x90

다음 읽기