지연 평가 (Lazy Evaluation)
지연 평가는 필요할 때까지 계산을 미루는 전략입니다. 반대는 즉시 평가(Eager Evaluation)입니다.
즉시 평가 vs 지연 평가
// 즉시 평가 — 모든 값을 먼저 계산
const result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.map(x => x * 2) // 10개 전부 변환
.filter(x => x > 10) // 10개 전부 검사
.slice(0, 3); // 그제야 3개만 취함
// 10 * 2번의 연산 후 3개만 사용
// 지연 평가 — 필요한 만큼만 계산
const lazyResult = take(3,
filter(x => x > 10,
map(x => x * 2,
range(1, 10))));
// 3개를 얻는 데 필요한 만큼만 연산JavaScript Generator로 지연 평가 구현
Generator 함수는 yield로 값을 하나씩 생성합니다. 요청이 있을 때만 다음 값을 계산합니다.
// 무한 수열 — 즉시 평가로는 불가능
function* naturals(start = 1): Generator<number> {
let n = start;
while (true) {
yield n++;
}
}
// 지연 map
function* lazyMap<T, U>(
iter: Iterable<T>,
fn: (x: T) => U,
): Generator<U> {
for (const x of iter) {
yield fn(x);
}
}
// 지연 filter
function* lazyFilter<T>(
iter: Iterable<T>,
pred: (x: T) => boolean,
): Generator<T> {
for (const x of iter) {
if (pred(x)) yield x;
}
}
// 앞 n개만 취하기
function* lazyTake<T>(iter: Iterable<T>, n: number): Generator<T> {
let count = 0;
for (const x of iter) {
if (count >= n) return;
yield x;
count++;
}
}
// 활용: 1부터 시작해 짝수인 것의 2배를, 처음 5개
const result = [...lazyTake(
lazyMap(
lazyFilter(naturals(), x => x % 2 === 0),
x => x * 2,
),
5,
)];
// [4, 8, 12, 16, 20]
// 무한 수열에서 필요한 만큼만 계산지연 파이프라인
체이닝 스타일로 만들면 더 읽기 좋습니다.
class LazySequence<T> {
constructor(private readonly source: Iterable<T>) {}
static from<T>(source: Iterable<T>): LazySequence<T> {
return new LazySequence(source);
}
static range(start: number, end?: number): LazySequence<number> {
return new LazySequence(
(function* () {
let i = start;
while (end === undefined || i <= end) yield i++;
})(),
);
}
map<U>(fn: (x: T) => U): LazySequence<U> {
const source = this.source;
return new LazySequence(
(function* () {
for (const x of source) yield fn(x);
})(),
);
}
filter(pred: (x: T) => boolean): LazySequence<T> {
const source = this.source;
return new LazySequence(
(function* () {
for (const x of source) if (pred(x)) yield x;
})(),
);
}
take(n: number): LazySequence<T> {
const source = this.source;
return new LazySequence(
(function* () {
let count = 0;
for (const x of source) {
if (count++ >= n) return;
yield x;
}
})(),
);
}
toArray(): T[] {
return [...this.source]; // 여기서 실제 계산 발생
}
first(): T | undefined {
for (const x of this.source) return x; // 첫 번째 하나만 계산
}
}
// 사용 — 선언적이고 읽기 쉬움, 실제 계산은 toArray/first에서
LazySequence.range(1) // 1, 2, 3, ... (무한)
.filter(x => x % 3 === 0) // 3의 배수
.map(x => x * x) // 제곱
.take(5) // 처음 5개
.toArray(); // [9, 36, 81, 144, 225]실용적인 예시
대용량 데이터 처리
// 파일을 줄 단위로 지연 처리 (Node.js)
async function* readLines(filePath: string): AsyncGenerator<string> {
const fileStream = createReadStream(filePath);
const rl = createInterface({ input: fileStream });
for await (const line of rl) {
yield line;
}
}
// 백만 줄짜리 CSV — 전부 메모리에 올리지 않고 처리
const errorLines = readLines('huge-log.csv');
for await (const line of lazyFilter(errorLines, l => l.includes('ERROR'))) {
console.log(line);
}비용 큰 계산의 지연
// 값을 처음 접근할 때만 계산하는 lazy 프로퍼티
function lazy<T>(compute: () => T): () => T {
let computed = false;
let value: T;
return () => {
if (!computed) {
value = compute();
computed = true;
}
return value;
};
}
const expensiveConfig = lazy(() => {
console.log('설정 로드 중...');
return loadConfig(); // 처음 호출 시 한 번만 실행
});
// expensiveConfig()를 호출하기 전까지 loadConfig는 실행 안 됨조건부 단락 평가
지연 평가는 &&, ||에도 이미 적용되어 있습니다.
// false && expensiveOp() — expensiveOp는 실행 안 됨
// true || expensiveOp() — expensiveOp는 실행 안 됨
// 함수로 표현
function lazyAnd(a: boolean, b: () => boolean): boolean {
return a && b(); // a가 false면 b()는 호출 안 됨
}무한 데이터 구조
지연 평가의 가장 강력한 응용입니다.
// 피보나치 수열 (무한)
function* fibonacci(): Generator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 처음 10개의 피보나치 수
const first10 = [...lazyTake(fibonacci(), 10)];
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// 100보다 큰 첫 번째 피보나치 수
const firstOver100 = LazySequence.from(fibonacci())
.filter(n => n > 100)
.first();
// 144정리
- 지연 평가: 필요할 때까지 계산 미룸 — 불필요한 연산 방지
- Generator: JavaScript의 지연 평가 메커니즘 —
yield로 값을 하나씩 생성 - 무한 시퀀스: 지연 평가가 있어야만 가능
지연 평가는 대용량 데이터 처리, 무한 시퀀스, 비용이 큰 계산의 지연에 유용합니다. 즉시 평가가 기본인 JavaScript에서 Generator를 활용해 필요한 부분에만 선택적으로 적용합니다.