합성·커링, 트랜스듀서, 제너레이터 기반 지연 평가, Result 패턴으로 선언적이고 견고한 데이터 흐름을 설계한다.
함수형 프로그래밍과 데이터 파이프라인
입문편에서 map·filter·reduce 같은 반복 메서드와 클로저를 익혔다면, 이제는 이 도구들을 조합해 데이터 흐름 자체를 설계하는 단계로 넘어갈 차례입니다. 함수형 스타일은 작은 순수 함수를 합성해 복잡한 변환을 만들고, 중간 배열을 줄여 성능을 높이며, 실패를 값으로 다뤄 예외 흐름을 명시적으로 만듭니다. 이번 레슨에서는 합성과 커링, 트랜스듀서, 제너레이터 기반 지연 평가, 그리고 Result 패턴까지 실무에서 바로 쓰는 함수형 설계 기법을 다룹니다.
학습 목표
- 순수 함수를
pipe·compose로 합성해 선언적 데이터 흐름을 구성할 수 있다. - **커링(currying)**과 **부분 적용(partial application)**으로 재사용 가능한 특수화 함수를 만들 수 있다.
- **트랜스듀서(transducer)**로 중간 배열 없이 변환을 합성해 메모리·성능을 개선할 수 있다.
- 제너레이터로 무한 스트림과 지연 평가(lazy evaluation) 파이프라인을 설계할 수 있다.
- Result/Either 패턴으로 예외 대신 값으로 실패를 다뤄 흐름을 명시화할 수 있다.
합성: pipe 와 compose
함수형 설계의 핵심은 작은 함수를 이어 붙여 하나의 변환으로 만드는 것입니다. pipe는 왼쪽에서 오른쪽으로, compose는 오른쪽에서 왼쪽으로 함수를 적용합니다.
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const dasherize = (s) => s.replace(/\s+/g, '-');
const slugify = pipe(trim, lower, dasherize);
console.log(slugify(' Hello World ')); // "hello-world"
각 단계는 입력 하나를 받아 출력 하나를 반환하는 **단항 함수(unary)**여야 깔끔하게 합성됩니다. 중간 변수 없이 데이터가 파이프를 따라 흐르므로, 읽는 순서가 곧 실행 순서가 됩니다.
💡 TIP
compose(f, g, h)(x)는f(g(h(x)))와 같습니다. 수학적 합성 표기에 익숙하면compose, 데이터 흐름을 위→아래로 읽고 싶으면pipe를 쓰세요.
커링과 부분 적용
커링은 다인자 함수를 "한 번에 한 인자씩" 받는 함수들의 연쇄로 바꿉니다. 인자가 충분히 모이면 그때 실행됩니다.
function curry(fn) {
return function curried(...args) {
// 인자가 충분히 모이면 실행, 아니면 나머지를 기다리는 함수 반환
if (args.length >= fn.length) return fn.apply(this, args);
return (...rest) => curried.apply(this, [...args, ...rest]);
};
}
const add = curry((a, b, c) => a + b + c);
console.log(add(1, 2, 3)); // 6
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2)(3)); // 6
커링하면 일부 인자를 미리 고정한 특수화 함수를 쉽게 만들 수 있어, pipe에 넣기 좋은 단항 함수가 됩니다.
const replace = curry((pattern, replacement, str) => str.replace(pattern, replacement));
const censor = replace(/\d+/g, '***'); // 숫자를 가리는 특수화 함수
const maskCard = pipe(
replace(/-/g, ''), // 하이픈 제거
censor,
);
console.log(maskCard('1234-5678')); // "***"
인자 순서를 바꾸기 어려운 함수에는 부분 적용이 더 간단합니다.
const partial = (fn, ...preset) => (...rest) => fn(...preset, ...rest);
const log = (level, message) => `[${level}] ${message}`;
const error = partial(log, 'ERROR');
console.log(error('연결 실패')); // "[ERROR] 연결 실패"
⚠️ 주의
fn.length는 기본값·나머지 매개변수(...args)를 세지 않습니다. 이런 함수를 자동 커링하면 의도와 다르게 동작하므로, 인자 개수를 명시적으로 넘기는curryN(arity, fn)형태를 쓰는 편이 안전합니다.
트랜스듀서로 중간 배열 없애기
arr.map(f).filter(p)는 읽기 쉽지만 중간 배열을 매 단계 생성합니다. 데이터가 크면 메모리와 순회 비용이 누적됩니다. 트랜스듀서는 "리듀서를 변환하는 함수"로, 변환 로직만 합성해 두고 단 한 번의 reduce로 처리합니다.
// 리듀서(step)를 받아 새 리듀서를 반환하는 변환들
const mapping = (f) => (step) => (acc, x) => step(acc, f(x));
const filtering = (pred) => (step) => (acc, x) => (pred(x) ? step(acc, x) : acc);
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
// 배열로 모으는 최종 리듀서
const toArray = (acc, x) => {
acc.push(x);
return acc;
};
const xform = compose(
mapping((x) => x * 2),
filtering((x) => x > 4),
);
const result = [1, 2, 3, 4].reduce(xform(toArray), []);
console.log(result); // [6, 8] — 중간 배열 없이 한 번의 순회
같은 xform을 배열이 아니라 합계 같은 다른 리듀서에도 그대로 재사용할 수 있다는 점이 트랜스듀서의 진짜 강점입니다.
const sum = (acc, x) => acc + x;
console.log([1, 2, 3, 4].reduce(xform(sum), 0)); // 14 (6 + 8)
제너레이터 기반 지연 평가
제너레이터는 값을 필요할 때 하나씩 만들어 내므로, 무한 수열이나 거대한 데이터에 파이프라인을 걸어도 메모리를 거의 쓰지 않습니다.
function* map(iter, f) {
for (const x of iter) yield f(x);
}
function* filter(iter, pred) {
for (const x of iter) if (pred(x)) yield x;
}
function* take(iter, n) {
let i = 0;
for (const x of iter) {
if (i++ >= n) return;
yield x;
}
}
function* naturals() {
let n = 1;
while (true) yield n++; // 무한 스트림
}
// 제곱 중 홀수만, 앞에서 3개
const odds = take(
filter(
map(naturals(), (x) => x * x),
(x) => x % 2 === 1,
),
3,
);
console.log([...odds]); // [1, 9, 25]
take(..., 3)이 3개를 받는 순간 naturals()는 더 이상 값을 만들지 않습니다. 소비하는 만큼만 계산되는 것이 지연 평가의 핵심입니다.
💡 TIP
for await...of와 비동기 제너레이터(async function*)를 쓰면 네트워크 페이지네이션·스트림 같은 비동기 소스에도 같은 파이프라인 패턴을 적용할 수 있습니다.
Result 패턴으로 실패를 값으로 다루기
예외(throw)는 함수 시그니처에 드러나지 않아 호출자가 놓치기 쉽습니다. Result(Either) 패턴은 성공/실패를 값으로 표현해, 합성하면서 안전하게 흐름을 이어 갑니다.
const Ok = (value) => ({ ok: true, value });
const Err = (error) => ({ ok: false, error });
// 성공일 때만 값을 변환
const mapR = (f) => (r) => (r.ok ? Ok(f(r.value)) : r);
// 성공일 때만 다음 Result-반환 함수로 연결
const chain = (f) => (r) => (r.ok ? f(r.value) : r);
const parseNumber = (s) => {
const n = Number(s);
return Number.isNaN(n) ? Err(`'${s}'는 숫자가 아닙니다`) : Ok(n);
};
const reciprocal = (n) => (n === 0 ? Err('0으로 나눌 수 없습니다') : Ok(1 / n));
const run = (input) =>
pipe(
parseNumber,
chain(reciprocal),
mapR((x) => x.toFixed(3)),
)(input);
console.log(run('4')); // { ok: true, value: "0.250" }
console.log(run('0')); // { ok: false, error: "0으로 나눌 수 없습니다" }
console.log(run('a')); // { ok: false, error: "'a'는 숫자가 아닙니다" }
한 단계라도 Err가 나오면 이후 chain·mapR는 그대로 통과시켜 **단락(short-circuit)**됩니다. 실패 경로가 값({ ok: false, error })으로 흐름에 드러나므로, 누락된 에러 처리를 리뷰 단계에서 발견하기 쉽습니다. 한 걸음 더 나아가 TypeScript로 Result를 판별 유니온(discriminated union)으로 타이핑하면, 누락된 분기를 컴파일 단계에서 잡을 수 있습니다.
⚠️ 주의 Result 패턴은 비동기와 섞일 때
Promise<Result>가 되어 중첩이 깊어질 수 있습니다. 이럴 때는async/await로 값을 풀어 쓰거나, 도메인 경계에서만 Result를 쓰고 내부는 예외를 쓰는 식으로 범위를 정하세요.
요약
pipe/compose는 단항 순수 함수를 합성해 선언적 데이터 흐름을 만든다.pipe는 좌→우,compose는 우→좌.- 커링과 부분 적용으로 인자를 미리 고정한 특수화 함수를 만들면 합성에 끼우기 좋다.
fn.length의 함정에 주의. - 트랜스듀서는 변환 로직만 합성해 한 번의
reduce로 처리하므로 중간 배열을 없애고, 리듀서를 바꿔 재사용할 수 있다. - 제너레이터는 소비하는 만큼만 계산하는 지연 평가로 무한 스트림·대용량 데이터를 메모리 효율적으로 다룬다.
- Result/Either 패턴은 실패를 값으로 표현해 합성 중 단락시키고, 에러 처리를 흐름에 명시한다.
연습문제
-
pipe를 직접 구현하고, 이를 이용해 문자열의 양끝 공백을 제거하고 → 소문자로 바꾸고 → 공백을_로 치환하는normalize함수를 작성하세요.힌트 각 단계는 문자열 하나를 받아 문자열 하나를 반환하는 단항 함수여야 합니다.
-
위에서 만든
curry를 사용해,(min, max, value) => value를 [min, max]로 clamp하는 함수를 커링하고,min=0, max=100으로 고정한percent함수를 만드세요.힌트
clamp(0)(100)호출의 결과가value만 받는 함수가 되도록 커링하세요. -
제너레이터
map·filter·take를 이용해, 1부터 무한히 증가하는 자연수 중 3의 배수의 제곱을 앞에서 4개 구하세요. 결과는[9, 36, 81, 144]가 되어야 합니다.힌트
filter로 3의 배수만 거르고 →map으로 제곱 →take(4)순서로 합성하세요. -
Ok/Err/chain을 이용해, 입력 객체에서age필드를 검증하는 파이프라인을 작성하세요. 필드가 없으면Err('age 없음'), 숫자가 아니면Err('age는 숫자'), 음수면Err('age는 0 이상'), 통과하면Ok(age)를 반환해야 합니다.힌트 검증 단계마다
Result를 반환하는 작은 함수를 만들고pipe와chain으로 연결하세요.
💡 연습문제 풀이
불러오는 중…
댓글 0
“JavaScript 심화” 강좌에 대한 댓글입니다.