개발일지/TIL(Today I Learned)

2024-11-07

프린스 알리 2024. 11. 7.

내일배움캠프 Node.js 트랙 9일차

1. ZEP에서 이루어진 자바스크립트 문법 스터디

오늘날 웹 애플리케이션과 모바일 앱 등 대부분의 프로그램은 네트워크 요청, 파일 시스템 작업, 사용자 입력 처리 등 다양한 비동기 작업을 수행합니다. 비동기 프로그래밍은 이러한 작업들을 효율적으로 처리하고 프로그램의 반응성과 성능을 높이는 데 필수적입니다. 비동기 작업을 동기적으로 처리할 경우, 프로그램은 특정 작업이 완료될 때까지 블로킹되어 다른 작업을 처리할 수 없게 됩니다. 이는 사용자 경험을 크게 저하시키고, 전반적인 프로그램 성능을 떨어뜨립니다.

 

자바스크립트는 본래 단일 스레드 기반의 언어로 설계되어 비동기 작업 처리에 어려움이 있었습니다. 초기에는 콜백 함수를 사용하여 비동기 작업을 처리했지만, 코드의 가독성과 유지보수성이 낮아지는 문제가 있었습니다. 이후 Promise, Generator, Async/await 등 비동기 코드를 더 효율적으로 작성할 수 있는 방식들이 도입되면서 자바스크립트의 비동기 프로그래밍 방식이 진화해왔습니다. 이러한 진화 과정을 통해 개발자들은 더 쉽고 간결하게 비동기 작업을 처리할 수 있게 되었습니다.

콜백 함수와 고차 함수 정의 및 역할

콜백 함수(callback function)는 다른 코드의 인자로 넘겨주는 함수를 말합니다. 반면에 고차 함수(higher-order function)는 함수를 인자로 받거나 함수를 반환하는 함수입니다. 고차 함수는 콜백 함수를 활용하여 다양한 역할을 수행합니다.

 

첫째, 고차 함수는 콜백 함수에 제어권을 넘겨줌으로써 비동기 작업을 처리할 수 있습니다. 예를 들어 setTimeout 함수는 일정 시간 후에 콜백 함수를 실행하는데, 이때 콜백 함수의 실행 시점을 결정하는 제어권이 setTimeout 함수에 있습니다. 이와 같이 콜백 함수는 제어권을 고차 함수에 넘겨주어 비동기 작업을 수행할 수 있습니다 [1].

 

둘째, 고차 함수는 콜백 함수를 통해 코드 재사용성을 높일 수 있습니다. 예를 들어 배열의 map, filter, reduce 등의 메서드는 콜백 함수를 인자로 받아 배열의 요소를 가공하는 로직을 재사용할 수 있게 합니다. 이렇게 콜백 함수를 활용하면 반복적인 코드를 줄일 수 있어 코드의 가독성과 유지보수성이 향상됩니다.

 

셋째, 고차 함수는 콜백 함수를 통해 추상화와 모듈화를 구현할 수 있습니다. 복잡한 로직을 콜백 함수로 분리하고 고차 함수에서 이를 호출하면, 코드를 더 추상화하고 모듈화할 수 있습니다. 이렇게 함으로써 코드의 구조를 개선하고 유지보수성을 높일 수 있습니다.

 

넷째, 콜백 함수는 this 바인딩 문제가 발생할 수 있으므로 주의해야 합니다. 콜백 함수 내부에서 this를 사용할 때는 bind 메서드를 활용하여 적절한 객체에 this를 바인딩해야 합니다 [1].

이처럼 콜백 함수와 고차 함수는 비동기 작업 처리, 코드 재사용, 추상화와 모듈화 등 다양한 측면에서 활용됩니다. 이를 적절히 활용하면 코드의 가독성과 유지보수성을 크게 향상시킬 수 있습니다.

콜백 함수의 this 바인딩 문제와 해결

콜백 함수를 사용할 때 주의해야 할 점은 this의 바인딩 문제입니다. 콜백 함수는 다른 함수의 인자로 전달되면서 원래 정의된 컨텍스트를 잃게 됩니다. 이로 인해 콜백 함수 내부에서 this를 잘못 참조하게 되어 의도치 않은 동작이 발생할 수 있습니다.

 

이러한 문제를 해결하기 위해서는 명시적으로 this의 값을 바인딩해야 합니다. 방법으로는 bind() 메서드를 사용하거나, call()/apply() 메서드를 사용하거나, 화살표 함수를 사용하는 것이 있습니다.

1) bind() 메서드를 사용하여 this 바인딩하기

const obj = {
  name: 'Alice',
  greet: function() {
    setTimeout(function() {
      console.log(`Hello, my name is ${this.name}`); // this는 undefined
    }.bind(this), 1000); // bind로 this를 바인딩
  }
}
obj.greet(); // Hello, my name is Alice

2) call()/apply() 메서드를 사용하여 this 바인딩하기

const obj = {
  name: 'Alice',
  greet: function() {
    setTimeout(function() {
      console.log(`Hello, my name is ${this.name}`);
    }.call(obj), 1000); // call로 this를 바인딩
  }
}
obj.greet(); // Hello, my name is Alice

3) 화살표 함수를 사용하여 this 바인딩하기

const obj = {
  name: 'Alice',
  greet: function() {
    setTimeout(() => { // 화살표 함수는 외부 this를 물려받음
      console.log(`Hello, my name is ${this.name}`);
    }, 1000);
  }
}
obj.greet(); // Hello, my name is Alice

이렇게 this 바인딩 방법을 사용하면 콜백 함수 내부에서 this를 올바르게 참조할 수 있습니다 [1]. 콜백 함수를 작성할 때는 반드시 this 바인딩 문제를 주의해야 합니다.

Promise 개념 및 체이닝

Promise는 비동기 작업의 최종 결과를 나타내는 객체입니다. 비동기 작업이 완료되면 Promise는 성공(resolved) 또는 실패(rejected) 상태가 됩니다. 이를 통해 비동기 작업의 결과를 동기적인 방식으로 처리할 수 있습니다.

 

Promise 체이닝은 연속적인 비동기 작업을 수행할 때 유용합니다. .then() 메서드를 사용하면 이전 Promise의 결과를 다음 Promise에 전달할 수 있습니다. 예를 들면 다음과 같습니다:

getData()
  .then(data => processData(data))
  .then(processedData => displayData(processedData))
  .catch(error => handleError(error));

위 코드에서 getData()는 비동기 데이터 요청을 수행하는 함수입니다. 요청이 성공하면 processData() 함수에 데이터를 전달하고, processData()의 결과는 displayData() 함수에 전달됩니다. 만약 어떤 단계에서 오류가 발생하면 catch() 메서드로 전달되어 handleError() 함수에서 처리됩니다 [1].

 

이처럼 Promise 체이닝을 사용하면 비동기 작업을 순차적으로 연결할 수 있어 코드의 가독성과 유지보수성이 높아집니다. 또한 콜백 지옥 문제를 방지할 수 있습니다 .

Promise의 에러 처리 및 장단점

Promise는 .catch() 메서드를 통해 에러를 처리합니다. Promise 체인 내에서 발생한 에러는 가장 가까운 .catch() 메서드로 전달되어 처리됩니다 [1]. 예를 들어:

getData()
  .then(processData)
  .then(displayData)
  .catch(handleError);

위 코드에서 getData(), processData(), displayData() 중 어느 단계에서 에러가 발생하더라도 handleError() 함수가 호출되어 에러를 처리할 수 있습니다.

 

Promise는 콜백 지옥 문제를 해결하고 비동기 코드의 가독성과 유지보수성을 높이는 장점이 있습니다. Promise 체이닝을 활용하면 비동기 작업을 연속적으로 수행할 수 있어 코드가 깔끔해집니다 [1]. 또한 에러 처리가 .catch() 메서드로 집중되어 있어 에러 처리 로직을 구조화하기 쉽습니다 .

 

하지만 Promise에도 단점이 있습니다. 복잡한 비동기 작업에서는 여전히 Promise 체이닝이 난해해질 수 있습니다. 또한 Promise는 구 브라우저에서 지원되지 않을 수 있으므로, 폴리필이 필요할 수 있습니다 . 이러한 단점을 해결하기 위해 ES8에서 async/await 문법이 도입되었습니다.

Generator 함수의 정의 및 동작

Generator 함수(Generator function)는 일반 함수와 유사하지만, 실행을 일시 중지하고 재개할 수 있는 특별한 함수입니다. Generator 함수는 function* 키워드로 정의되며, 함수 내부에 yield 키워드를 사용하여 실행을 일시 중지하고 값을 반환할 수 있습니다.

 

Generator 함수의 동작 원리는 다음과 같습니다:

  1. Generator 함수가 호출되면 실행되지 않고 Generator 객체를 반환합니다.
  2. Generator 객체의 next() 메서드를 호출하면 Generator 함수가 실행되기 시작합니다.
  3. 함수 내부에서 yield 키워드를 만나면 실행이 일시 중지되고, yield 뒤의 값이 반환됩니다.
  4. next() 메서드를 다시 호출하면 중지된 지점부터 실행이 재개되고, 다음 yield 키워드를 만날 때까지 진행됩니다.
  5. 더 이상 yield 키워드가 없으면 함수가 종료되고, done 프로퍼티가 true인 객체가 반환됩니다.

이렇게 Generator 함수는 실행 상태를 저장하고 재개할 수 있어서 비동기 작업의 동기적 표현이 가능합니다. 예를 들어, 비동기 API 호출 결과를 yield 키워드로 반환하고, 다음 next() 호출 시 결과를 사용할 수 있습니다. 이를 통해 비동기 코드를 마치 동기 코드처럼 작성할 수 있습니다.

 

Generator 함수는 비동기 작업을 동기적으로 표현할 수 있다는 장점이 있지만, 코드의 가독성이 다소 떨어지고 Promise와의 호환성 문제가 있습니다. 이런 단점을 보완하기 위해 ES8에서 async/await 문법이 도입되었습니다 [1].

Generator를 사용한 비동기 작업 처리 및 장단점

Generator 함수를 활용하면 비동기 작업을 동기적인 코드 흐름으로 표현할 수 있습니다. 다음 예시를 통해 Generator가 비동기 작업을 처리하는 방식을 살펴보겠습니다:

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('데이터 fetching 완료');
    }, 1000);
  });
}

function* getDataGenerator() {
  const data = yield fetchData();
  console.log(data);
}

const generator = getDataGenerator();
const fetchPromise = generator.next().value;

fetchPromise.then(result => {
  generator.next(result);
});

위 코드에서 getDataGenerator() 함수는 Generator 함수입니다. fetchData() 함수는 비동기 데이터 요청을 모방하는 Promise를 반환합니다. getDataGenerator() 함수가 호출되면 fetchData() 함수의 Promise 객체가 반환되고, 이를 fetchPromise 변수에 할당합니다.

 

fetchPromise의 then() 메서드에서 fetchData()의 결과를 generator.next(result)로 전달하면, Generator 함수 내부의 yield fetchData() 문이 result 값으로 대체되고 실행이 재개됩니다. 이후 console.log(data)가 실행되어 "데이터 fetching 완료"가 출력됩니다.

 

이처럼 Generator를 사용하면 비동기 작업의 결과를 동기적인 코드 흐름으로 표현할 수 있습니다. 이는 비동기 코드의 가독성과 유지보수성을 높이는 장점이 있습니다.

 

하지만 Generator에는 단점도 있습니다. 첫째, Promise와의 호환성 문제가 있습니다. Generator 함수에서 Promise를 사용하려면 추가적인 코드가 필요합니다. 둘째, 코드의 가독성이 다소 떨어질 수 있습니다. Generator 함수의 실행 흐름이 next() 메서드 호출로 인해 분산되어 있어 코드를 이해하기 어려울 수 있습니다. 셋째, ES6 이전 버전에서는 Generator를 사용할 수 없으므로 폴리필이 필요합니다 [1].

 

Generator는 비동기 코드를 동기적으로 표현할 수 있다는 장점이 있지만, 다소 복잡한 구현 방식과 호환성 문제 등의 단점도 있습니다. 이러한 단점을 극복하기 위해 ES8에서 async/await 문법이 도입되었습니다.

Async/Await 구문 소개 및 Promise와의 연계성

Async/await는 ES8(ECMAScript 2017)에서 도입된 비동기 코드 작성 방식입니다. 이 구문은 Promise를 기반으로 하면서도, 마치 동기 코드를 작성하는 것처럼 코드의 가독성과 유지보수성을 높여줍니다.

 

async 키워드를 함수 앞에 붙이면 해당 함수는 Promise를 암묵적으로 반환하게 됩니다. 그리고 async 함수 내부에서 await 키워드를 사용하면 Promise가 settled(fulfilled 또는 rejected)될 때까지 대기하고, 그 결과 값을 변수에 할당할 수 있습니다. 예를 들면 다음과 같습니다:

async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return data;
}

fetchData().then(data => console.log(data));

위 예시에서 fetchData() 함수는 async 함수이므로 Promise를 반환합니다. await 키워드를 사용하여 fetch() 함수의 Promise 결과를 기다린 후, 응답 데이터를 처리합니다. 이렇게 async/await 구문을 사용하면 비동기 작업을 동기적인 코드 흐름으로 표현할 수 있어 가독성과 유지보수성이 높아집니다 [1].

 

async/await는 Promise와 밀접하게 연계되어 있습니다. async 함수는 Promise를 반환하고, await 키워드는 Promise가 settled되기를 기다립니다. 따라서 async/await 구문을 사용하면서도 Promise 체이닝과 에러 핸들링을 활용할 수 있습니다.

 

async/await의 장점은 복잡한 비동기 코드를 단순화할 수 있고, try/catch 구문으로 에러 처리가 용이하다는 점입니다. 하지만 async/await 구문도 Promise와 마찬가지로 구 브라우저에서는 지원되지 않을 수 있으므로 주의해야 합니다 .

또 다른 예제

  • Promise만 사용
    // fetch API는 Response 객체를 Resolving하는 Promise를 반환합니다.
    // Response의 body 값에 접근하는 방법은 Response.json() 입니다.
    function fetchAndPrintJson(url) {
    return fetch(url)
      .then((response) => response.json()) // 반환된 Promise에서 Response 객체의 body 값에 접근
      .then((response) => console.log(response)); // response.json을 console.log로 출력
    }
    fetchAndPrintJson("https://jsonplaceholder.typicode.com/posts/1");
     
  •  Async/Await
// fetch API는 Response 객체를 Resolving하는 Promise를 반환합니다.
// await Promise(result) 는 result 값을 반환해 줍니다.
async function fetchJson(url) {
  const response = await fetch(url); // fetch는 Promise를 반환
  const data = await response.json(); // await Promise(result) 는 result 값을 반환

  return console.log(data);
}

fetchJson("https://jsonplaceholder.typicode.com/posts/1");

Async/Await의 에러 처리 및 장단점

Async/await에서는 try-catch 블록을 사용하여 에러를 처리할 수 있습니다. async 함수 내에서 await 키워드로 Promise를 기다리는 부분을 try 블록으로 감싸고, catch 블록에서 발생한 에러를 처리하면 됩니다. 예를 들어:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching data:', error);
    throw error; // 에러를 다시 throw하여 상위 단계에서 처리
  }
}

위 예시에서 fetch() 함수의 Promise가 거부되면 catch 블록으로 이동하여 에러를 처리할 수 있습니다. 이렇게 try-catch 구문을 사용하면 에러 처리 로직을 구조화할 수 있어 코드의 가독성과 유지보수성이 향상됩니다 .

 

Async/await의 장점은 비동기 작업을 동기적으로 표현할 수 있어 코드의 가독성이 높다는 것입니다. Promise 체이닝보다 코드 흐름이 직관적이어서 복잡한 비동기 작업도 쉽게 처리할 수 있습니다. 또한 에러 처리도 try-catch 구문으로 간단해집니다 .

 

하지만 async/await에도 단점이 있습니다. 첫째, 구 브라우저에서는 지원되지 않을 수 있어 폴리필이 필요할 수 있습니다. 둘째, Promise 체이닝보다 가독성이 떨어질 수 있는 경우가 있습니다. 셋째, async 함수 내에서 반복문이나 조건문을 사용할 때 주의해야 합니다. 반복문이나 조건문 내에서 await를 사용하면 의도치 않은 동작이 발생할 수 있기 때문입니다 .

'개발일지 > TIL(Today I Learned)' 카테고리의 다른 글

2024-11-11  (5) 2024.11.11
2024-11-08  (3) 2024.11.08
2024-11-06  (0) 2024.11.06
2024-11-05  (4) 2024.11.05
2024-11-04  (0) 2024.11.04

댓글