오늘은 이번 학기 수업에서 들었던 가장 임팩트 있는 내용 중 하나를 살펴보려 한다. 나는 미국에서 컴퓨터 과학(Computer Science)을 공부하고 있으며, 프로그래밍에 사용되는 개념들을 익혀나가고 있다.
참고로 이 글은 개인적인 학습 기록이므로, 독자의 지식 수준이나 범위를 특별히 고려하지 않았다. 혹여나 수정하고 싶거나 오류가 있는 내용이 있다면 댓글과 메일로 알려주시면 전적으로 감사하겠다.
프로그래밍이 갖춰야 하는 기본 원칙은 매우 많고 넓은 팩터들이 있지만, 그중에서도 불변성(Immutability)과 최근 트렌드인 타입 안정성(Type Safety)을 모두 반영하고 보여주는 map에 대해 간략히 소개하고자 한다.
불변성이란 무엇인가
map을 왜 써야 하는가를 설명하기 위해, 필연적인 베이스 지식 중 하나인 불변성부터 짚고 넘어가겠다.
프로그래밍에서는 (뿐만 아니라 IT 전반, 인프라 포함) 불변성이 매우 높은 가치를 지닌다. 내 생각에는 이는 프로그래밍이 복잡한 상태 변화를 다루는 과정에서, 예측 불가능한 변화를 최소화하고 제어 가능한 환경을 만들려는 노력의 일환이라고 생각한다.
여튼, 원본을 유지하고 복사본을 활용하여 여러 가지 연산을 하게 되면 아래와 같은 장점을 가져올 수 있다:
1. 코드의 산출물이 예측 가능해진다
- 이를 통해서 발생하는 사이드 이펙트를 예상, 제어, 제거할 수 있다.
2. 디버깅이 용이하다
- 여러 가지 조건이 달린 for문이나 for문 안에 for문이 들어가는 다중 for문과 비교하면 너무나 행복하다.
3. 병렬 처리가 안전하다
- 원본이 변하지 않으니 여러 곳에서 동시에 사용해도 안전하다.
그러면 의문점이 하나 생긴다. 왜 모든 언어에서 이러한 함수형 메서드를 기본 제공하지 않는가? 여기에는 언어의 패러다임 차이(명령형 vs 함수형)와 설계 철학에 관한 더 넓은 논의가 필요해서 이 포스트에서 모두 다루지는 못할 듯하다. 혹시나 궁금하다면 추후 포스팅을 참고했으면 한다.
for문 vs map: 코드로 보는 차이
잠시 조금 멀리 논의했지만, 일단 코드를 보고 생각해보자.
// for문: 원본 수정 or 새 배열 수동 관리
const numbers = [1, 2, 3];
for(let i = 0; i < numbers.length; i++) {
numbers[i] = numbers[i] * 2; // 원본 변경!
}
// map: 항상 새 배열 반환
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2); // 원본 안전
console.log(numbers); // [1, 2, 3] - 원본 유지!
어떤가?
일단 코드 줄이 줄어든 것도 줄어든 것이지만, 원본 데이터에 변화가 없다는 불변성을 간단하게 이루어낼 수 있다.
타입 안정성: map의 또 다른 장점
과거 for문을 남발하던 나의 코딩 스타일(물론 VBS처럼 함수의 제약이 많은 언어도 있지만)에서 가장 문제였던 부분 중 하나는 저 복잡한 for문의 과정에서 발생하는 타입을 추적하기였다.
어떨 때는 string으로 데이터를 뽑기도 하고, int와 float이 섞이기 시작하면 원본이 뭐였는지를 찾기 위해 코드베이스를 위아래로 탐색하던 경험이 있다. map은 이러한 고통을 간단하게 바꾸어낼 수 있다.
TypeScript를 예로 들면:
// for문: 타입 추론 어려움
const numbers: number[] = [1, 2, 3];
const result = []; // any[]로 추론될 수 있음
for(let n of numbers) {
result.push(n.toString());
}
// map: 타입이 명확
const numbers: number[] = [1, 2, 3];
const strings: string[] = numbers.map(n => n.toString());
// number[] → string[] 타입 변환 명확
Imperative vs Declarative: 사고방식의 차이
교수님은 수업에서 for문을 “imperative(명령형)”하다고 표현하셨다. for문은 “어떻게(HOW)” 반복할지를 단계별로 명령하는 방식이다.
// Imperative: "어떻게" 할지 단계별로 명령
const result = [];
for(let i = 0; i < numbers.length; i++) {
result.push(numbers[i] * 2);
}
// 1. 빈 배열 만들어
// 2. 인덱스 0부터 시작해
// 3. 하나씩 꺼내서
// 4. 곱하기 2 해서
// 5. result에 넣어
반면 map은 “declarative(선언형)” 스타일이다. “무엇을(WHAT)” 원하는지만 선언하면, 구체적인 반복 방법은 추상화되어 있다.
// Declarative: "무엇을" 원하는지 선언
const result = numbers.map(n => n * 2);
// "각 숫자를 2배로 변환한 배열이 필요해"
이것이 map이 더 읽기 쉽고 의도가 명확한 이유다.
Pure Function: map의 본질
교수님은 동시에 map과 재귀를 “pure(순수)”하다고 강조하셨다. 이것이 정말 중요한 개념이다.
Pure Function(순수 함수)이란:
- 같은 입력에 항상 같은 출력을 반환
- 외부 상태를 변경하지 않음 (no side effects)
- 외부 상태에 의존하지 않음
// ❌ Impure: 외부 변수 수정
const numbers = [1, 2, 3];
const result = [];
for(let i = 0; i < numbers.length; i++) {
result.push(numbers[i] * 2); // result라는 외부 상태 변경
}
// ✅ Pure: 외부 상태 변경 없음
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2); // 새로운 배열만 반환
Pure Function이 중요한 이유:
- 테스트가 쉽다: 입력만 주면 출력을 예측할 수 있다
- 디버깅이 쉽다: 함수 하나만 보면 된다
- 병렬 처리가 가능하다: 다른 함수에 영향을 주지 않는다
- 캐싱이 가능하다: 같은 입력이면 결과를 재사용할 수 있다 (memoization)
map의 재귀적 구현: 개념의 정수
map이 최근에 나온 신기능이라든지, map이면 모든 게 다 해결된다는 만능론을 주장하려는 것이 아니다. 지금까지 map을 사용해보지 못한 프로그래밍 초짜라서 이게 의미 있다는 것도 아니다.
무심코 사용하는 map에 이러한 장점이 있다는 것 자체가 센세이션한 경험이었다.
이러한 구현체로서 map이 장점을 지녔다고 설명할 수 있지만, 한 발 더 나아가서 이러한 구현체를 어떻게 만들 것인가가 수업에서 본 재밌는 예제였다.
수업에서는 재귀 함수를 통해 map을 구현하는 것을 볼 수 있었다:
// map의 재귀적 구현
function myMap(arr, fn) {
// 기저 조건: 빈 배열이면 빈 배열 반환
if (arr.length === 0) return [];
// 재귀 호출: 첫 번째 원소를 변환하고, 나머지에 대해 재귀
const [first, ...rest] = arr;
return [fn(first), ...myMap(rest, fn)];
}
// 사용 예시
const numbers = [1, 2, 3];
const doubled = myMap(numbers, n => n * 2);
console.log(doubled); // [2, 4, 6]
console.log(numbers); // [1, 2, 3] - 원본 유지!
이 재귀 함수를 보면 모든 개념이 하나로 연결된다:
1. Pure Function
- 어떤 외부 변수도 건드리지 않는다
- 순수하게 입력을 받아 새로운 출력을 만들어낼 뿐이다
2. Immutability (불변성)
- 매 재귀 단계마다 새로운 배열을 생성
- 원본이 절대 변하지 않는다
3. Type Safety (타입 안정성)
- 입력 배열의 타입 → 변환 함수 → 출력 배열의 타입이 명확하게 이어진다
4. Declarative (선언형)
- “첫 번째를 변환하고, 나머지도 같은 방식으로”라고 선언
- HOW(어떻게 반복하는지)가 아닌 WHAT(무엇을 원하는지)에 집중
물론 실제 JavaScript의 Array.prototype.map은 성능을 위해 내부적으로 반복문을 사용한다. 하지만 개념적으로는 이러한 재귀적 사고를 담고 있으며, 그것이 map이 강력한 이유다.
그렇다면 for문은 언제 쓸까?
물론 map이 만능은 아니다. 다음과 같은 경우에는 for문이 더 적합할 수 있다:
break나continue가 필요한 경우- 극도로 성능이 중요한 경우 (매우 큰 배열, 실시간 처리)
- 매우 복잡한 제어 로직이 필요한 경우
- 여러 배열을 동시에 순회해야 하는 경우
하지만 일반적인 배열 변환 작업에서는 map이 훨씬 더 안전하고 명확한 선택이다.
마무리
결론적으로, map은 단순한 편의 기능이 아니라 함수형 프로그래밍의 핵심 원칙들을 담고 있는 구조였다:
- Pure Function: 예측 가능하고 안전한 함수
- Immutability: 원본을 건드리지 않는 철학
- Type Safety: 명확한 타입 변환
- Declarative: 의도가 명확한 코드
이것이 바로 함수형 프로그래밍의 핵심이며, React의 state 관리, Redux의 reducer 패턴 등 현대 프로그래밍의 많은 부분에 이러한 철학이 녹아있다.
이제 map을 쓸 때마다 그 속에 담긴 깊은 철학을 떠올릴 수 있을 것 같다. 단순히 “편리한 메서드”가 아니라, 더 나은 프로그래밍을 위한 사고방식의 전환이었던 것이다.