Javascript

즉시 평가, 지연 평가 - JavaScript

아가프린 2024. 9. 25. 15:45

JavaScript에는 즉시 평가와 지연 평가라는 개념이 있습니다.

 

저는 즉시 평가와 지연 평가들에 대한 자료를 보고 "어떨 때 평가를 하는구나"

정도만 알고 평가가 무엇인지 정확히는 몰랐습니다.

 

그래서 저는 즉시와 지연을 작성하기 전에 js에서 평가가 무엇인지 작성할테니 참고하고 보시면 좋을 것 같습니다.

평가

아래의 과정들을 포괄해 평가라고 부릅니다.

코드 실행: JavaScript 엔진이 코드를 파싱하고 실행하는 과정
값 도출: 표현식이나 문장으로부터 최종적인 값을 계산해내는 과정으로 연산자 적용, 함수 호출 결과 계산 등을 포함 
타입 결정: 표현식의 결과가 어떤 데이터 타입인지 결정하는 과정
변수 바인딩: 변수에 값을 할당하는 과정으로 이는 변수 선언과 초기화를 포함
함수 실행: 함수 호출 시 인자를 평가하고 함수 본문을 실행하는 과정

 

즉시 평가

즉시 평가는 표현식이 작성 된 순간 값을 즉시 평가합니다.

일반적으로 변수 선언과 배열 메서드, 문자열 메서드 등이 이에 해당합니다. (promise도 포함!)

const a = 10 + 20; // 30 즉시 평가
const upperHello = "hello".toUpperCase(); // HELLO 즉시 평가
const doubleNums = [1, 2, 3, 4, 5].map((v) => v * 2); // [2, 4, 6, 8, 10] 즉시 평가
console.log(Math.random()); // 랜덤한 값 즉시 평가

 

즉시 평가라는 게 무엇인지 감이 오시나요?

 

근데 여기서 의문점이 생길 수 있습니다. Promise는 왜 즉시 평가에 포함이 될까요?

생길 수 있는 이유는 아마 이렇게 생각해서 그런 것 같습니다.

Promise를 생성해도 then이나 catch를 실행해야 값이 도출된다. 등으로 말이죠.

 

Promise가 즉시 평가에 해당하는 이유는 다음과 같습니다.

  1. 즉시 실행
    Promise 생성자에 전달되는 executor 함수는 promise가 생성되는 즉시 실행되고 promise 객체도
    지연되지 않고 즉시 생성됩니다. then이나 catch를 사용해야 값이 도출되는 걸 보고 이건 지연 평가가 아니냐고 할 수 있지만
    이건 지연 평가가 아니라 지연 처리의 영역으로 봐야합니다.
    setTimeout이 지연 처리에 해당하는데 setTimeout 함수 자체는 즉시 실행되지만 처리 자체는 지연시킵니다.
  2. 되돌릴 수 없는 작업
    일단 executor 함수가 실행되기 시작하면 외부에서 중지하거나 일시적으로 중지시킬 수 없습니다.
    함수가 수행하는 작업의 결과는 가능한 빨리 처리되도록 자바스크립트 이벤트 루프에 대기열로 추가됩니다.
  3. 지연 옵션 x
    프로미스에는 값이 필요할 때까지 실행자의 실행을 연기하거나 취소하는 내장 메커니즘이 없습니다.
  4. 부작용
    Promise의 즉시 처리 특성은 executor 함수에 포함된 부작용(예: API 호출, 타임아웃 또는 I/O 작업)이
    프로미스 생성과 함께 즉시 발생한다는 것을 의미합니다.
console.log("첫 번째 출력");

const promise = new Promise((resolve) => {
  // executor 영역
  console.log("프로미스 executor 실행");
  resolve("프로미스 resolve 실행");
});

console.log("promise 호출 전");

promise.then((result) => console.log(result));

첫 번째 출력
프로미스 executor 실행
promise 호출 전
프로미스 resolve 실행

 

지연 평가

즉시 평가에 대해서 이 정도만 알고 있어도 이 다음을 알기 훨씬 쉬울 것입니다.

그렇다면 지연 평가는 무엇이고 왜 사용해야 할까요?

 

지연 평가는 즉시 평가와 반대로 값이 실제로 필요할 때까지 평가를 미루는 방식입니다.

필요할 때만 계산하기에 불필요한 연산을 줄일 수 있습니다.

 

JS에서 지연 평가의 예시에는 || && 논리 연산자, 제너레이터 등이 있습니다.

논리 연산자는 왜 지연 평가일까요?

function double(num) {
  return num * 2;
}

const num = 5;
const flag = false;

const result = flag && double(num);
console.log("결과:", result); // false

 

이런 형태를 단락 평가라고 하는데 단락 평가는 지연 평가의 한 종류입니다.

위 코드에서 flag가 true가 아니라면 즉 false라면 double을 실행하지 않겠죠?

값이야 바로 도출되겠지만 불필요한 연산을 하지 않습니다.

 

불필요한 연산을 줄이는 거 외에도 지연 평가를 이용하면 값의 크기에 제한이 없습니다.

한 번에 너무나 많은 양을 처리한다면 당연히 문제가 생기고 최적화를 진행해야 합니다.

그런데 지연 평가는 값이 필요할 때만 계속 요청을 하여 연산을 진행하기에 흔히 무한 시퀀스라고

부르는 작업을 처리하기에 유용합니다. 스크립트가 멈추지 않으니까요.

function* infiniteSequence() {
  let i = 0;
  while (true) {
    yield i++;
  }
}

const numbers = infiniteSequence();
console.log(numbers.next().value); // 0
console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2

일반 함수였다면 어떻게 됐을까요?

호출 된 순간 무한 루프에 걸리게 될 것입니다!

 

제너레이터 함수

js에서 대표적으로 지연 평가를 구현할 수 있는 기능입니다.

물론 일반 함수에서도 지연 평가를 구현할 수는 있지만 제너레이터는 지연 평가를 위해 나온 기능이기에

제너레이터 함수를 사용하는 것을 추천합니다.

 

어떻게 동작하기에 지연 평가가 가능할까요?

함수 내의 작업을 일시적으로 중지했다가 재개할 수 있는 특수한 함수입니다!

함수 내부에서 처음 yield를 제외하고 yield 문을 만나지 않으면 작업을 계속 수행하고

yield 문을 만나게 되면 작업을 중지합니다.

 

그래서 값이 필요한 순간에만 호출할 수 있습니다.

위 예시를 보니 이해가 되시나요?

 

제너레이터 함수는 일반 함수와 달리 호출할 때 작업을 즉시 실행하지 않습니다.

제너레이터는 변수에 할당됐을 때 next, return, throw라는 3가지 메서드를 반환합니다.

 

next

next는 중지 되었던 작업을 다시 재개할 수 있게 해주는 메서드입니다.

next를 실행하면 done과 value 프로퍼티를 가진 객체를 반환합니다.

function* generatorSequence() {
  console.log("하나");
  yield 1;
  console.log("둘");
  yield 2;
  console.log("셋");
  yield 3;
  console.log("넷");
  return 4;
}

const generator = generatorSequence();

generator.next();
// output:
// "하나"
// {value: 1, done: false}
generator.next();
// output:
// "둘"
// {value: 2, done: false}
generator.next();
// output:
// "셋"
// {value: 3, done: false}
generator.next();
// output:
// "넷"
// {value: 4, done: true}
generator.next();
// output: {value: undefined, done: true}

done은 현재 함수 내의 작업이 완료되었는지(return 되었는지)를 boolean으로 나타내고

value는 yield가 반환하는 값입니다. 이렇게 현재 필요한 값을 yield로 반환하는 것이죠.

 

return

제너레이트 함수는 return을 만나야 작업을 완료했다는 것을 알 수 있습니다.

그런데 제너레이터 함수 내에서 작업을 next로 진행하다가 갑자기 즉시 종료를 해야할 일이 생기면

return이 나타날 때까지 next를 할 수는 없겠죠?

이럴 때 return을 사용할 수 있습니다. 강제로 중간에 return 문을 심어주는 것 같네요.

tru/finally와 같이 사용합니다.

function* generatorSequence() {
  try {
    console.log("하나");
    yield 1;
    console.log("둘");
    yield 2;
    console.log("셋");
    yield 3;
    console.log("넷");
    return 4;
  } finally {
    console.log("제너레이터 정리 작업");
  }
}

const generator = generatorSequence();

console.log(generator.next());
// output:
// "하나"
// {value: 1, done: false}

console.log(generator.next());
// output:
// "둘"
// {value: 2, done: false}

console.log(generator.return(100));
// output:
// "제너레이터 정리 작업"
// {value: 100, done: true}

console.log(generator.next());
// output: {value: undefined, done: true}

console.log(generator.next());
// output: {value: undefined, done: true}

 

return 메서드를 사용한 후로 아무리 next를 해봐도 아무것도 도출하지 않습니다.

다시 next를 하고 싶다면 새롭게 생성자를 생성하면 됩니다.

 

throw

return과 비슷하지만 정상적으로 종료하는 것이 아니라 일부러 예외를 발생시키는

방식으로 제너레이터 함수를 중지합니다.

function* numberGenerator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (error) {
    console.log("제너레이터 내부에서 에러 처리:", error.message);
    yield "에러 후 복구";
  }
  yield 4;
}

const gen = numberGenerator();

console.log(gen.next().value); // 1
console.log(gen.next().value); // 2

try {
  console.log(gen.throw(new Error("의도적인 에러 발생")));
  // "제너레이터 내부에서 에러 처리: 의도적인 에러 발생"
  // { value: '에러 후 복구', done: false }
} catch (e) {
  console.log("제너레이터 외부에서 에러 처리:", e.message);
}

console.log(gen.next().value); // 4

 

정리

JS에서 제너레이터 함수는 잘 사용하지 않아서 대충만 알고 넘어갔던 것 같은데

이렇게 자료를 찾아보고 여러 생성형 AI들과 생각을 공유하고 배워서 좋았습니다!

즉시 평가와 지연 평가도 추상적으로 대충 언제 JS 엔진이 값을 알구나 정도만 알고 제대로 공부해본 적은 처음이었고

추상적이었던 개념을 제 지식으로 만들어서 좋네요.