Callbacks (feat. rxjs)

h232ch·2023년 8월 5일
0
post-thumbnail

자바스크립트 비동기 처리

자바스크립트 특징

자바스크립트는 기본적으로 비동기 처리를 수행한다. 비동기 처리란 일련의 코드를 동시에 실행하는 것을 말한다. 특정 코드의 연산이 종료되지 않더라도 다음 코드가 실행되는 특성이다.

// #1
console.log('Hello');
// #2
setTimeout(function () {
  console.log('Bye');
}, 3000);
// #3
console.log('Hello Again')

아래 코드를 확인해보면 자바스크립트 비동기 처리가 어떻게 동작하는지 확인할 수 있다. 코드가 순차적으로 수행되는 것처럼 보이나 실제로 setTimeout이 수행되는 코드는 3초 뒤에 수행되어 마지막 콘솔 로그에 출력된다. 이는 setTimeout 함수가 콜백 함수로서 익명 함수를 3초 뒤에 실행한다는 것을 보여준다.

콜백 함수는 매개변수로 함수 객체를 전달하여 호출 함수 내에서 매개변수 함수를 실행하는 것을 의미한다. 아래 코드는 sayHello 함수를 호출하면서 callback 함수인 printing(name)를 전달하고 sayHello 함수 내부에서 다시 callback을 호출하여 마지막으로 sayHello 함수 내부의 console.log(name)이 호출되는 과정을 확인할 수 있다.

콜백 동작 순서

  1. sayHello("인파", function printing(name)
  2. function sayHello(name, callback)
  3. const words = ...
  4. callback(words)
  5. sayHello... console.log(name);

콜백 함수란 일반적인 변수나 값을 전달하는 것이 아닌 함수 자체를 전달 하는 것을 의미한다. 매개 변수로서 한번 사용되기 때문에 일반적으로 익명 함수로 사용한다.

function sayHello(name, callback) {
    const words = '안녕하세요 내 이름은 ' + name + ' 입니다.';
    
    callback(words); // 매개변수의 함수(콜백 함수) 호출
}

sayHello("인파", function printing(name) {
	console.log(name); // 안녕하세요 내 이름은 인파 입니다.
});

콜백 함수 사용 원칙

익명 함수 사용

익명 함수를 사용하는 이유중 하나는 코드 간결성이다. 콜백 함수는 보통 매개변수로서 사용되기 때문에 함수의 이름이 구지 필요하지 않다. 두번째 이유는 함수 이름 충돌 방지이다. 콜백 함수에 이름을 붙이면 함수 스코프 내에서 유효한 식별자가 되는데 만약 같은 스코프 내에 같은 이름의 식별자가 존재하는 경우 기존 식별자를 덮어쓰게되어 버린다. 이는 의도치않은 결과를 낳고 오류를 발생시키는 원인이 된다.

let add = 10; // 변수 add

function sum(x, y, callback) {
  callback(x + y); // 콜백함수 호출
}

// 이름 있는 콜백함수 작성
sum(1, 2, function add(result) {
  console.log(result); // 3
});

// 변수 add가 함수 add가 되어버린다.
console.log(add); // function add(result) {...}

화살표 함수 모양의 콜백

콜백 함수로 익명 함수를 사용함으로써 간결성을 유지할 수 있다고 언급했다. 이것에 더 나아가 간결성있는 코드를 작성하기 위해 익명 화살표 함수 형태로 정의하여 사용할 수 있다.

function sayHello(callback) {
  var name = "Alice";
  callback(name); // 콜백 함수 호출
}

// 익명 화살표 콜백 함수
sayHello((name) => {
  console.log("Hello, " + name);
}); // Hello, Alice

// 익명 콜백 함수
sayHello(function(name) {
	consol.log("hello, " + name);		
});

함수의 이름 넘기기

만약 콜백 함수가 일회용이 아니라 여러 호출 함수에 재활용되는 경우 콜백 함수 자체를 별도로 정의하여 이름만 호출 함수의 인자에 전달하는 방식으로 사용 가능하다.

// 콜백 함수를 별도의 함수로 정의
function greet(name) {
  console.log("Hello, " + name);
}

function sayHello(callback) {
  var name = "Alice";
  callback(name); // 콜백 함수 호출
}

function sayHello2(callback) {
  var name = "Inpa";
  callback(name); // 콜백 함수 호출
}

// 콜백 함수의 이름만 인자로 전달
sayHello(greet); // Hello, Alice
sayHello2(greet); // Hello, Inpa

이러한 특징을 이용하면 매개 변수로 전달되는 콜백 함수의 종류만 바꿔줌으로 여려가지 함수 형태를 다양한 상황에 맞게 전달 가능하다.

function introduce (lastName, firstName, callback) {
    var fullName = lastName + firstName;
    
    callback(fullName);
}

function say_hello (name) {
    console.log("안녕하세요 제 이름은 " + name + "입니다");
}

function say_bye (name) {
    console.log("지금까지 " + name + "이었습니다. 안녕히계세요");
}

introduce("홍", "길동", say_hello);
// 결과 -> 안녕하세요 제 이름은 홍길동입니다

introduce("홍", "길동", say_bye);
// 결과 -> 지금까지 홍길동이었습니다. 안녕히계세요

콜백 함수의 활용 사례

이벤트 리스너

addEventListner는 특정 이벤트가 발생했을 때 콜백 함수를 실행하는 메서드이다. 클릭과 같은 이벤트를 처리하기 위해 이벤트 리스너로 콜백 함수가 사용된다. 버튼을 클릭하는 경우 익명 함수로 전달된 콜백 함수가 실행되는 형태이다.

let button = document.getElementById("button"); // 버튼 요소를 선택

// 버튼에 클릭 이벤트 리스너를 추가
button.addEventListener("click", function () { // 콜백 함수
  console.log("Button clicked!"); 
});

고차함수에 사용

forEach 메서드도 콜백 함수를 사용한다. forEach 함수의 익명 함수에서 doubled 배열에 2를 곱한 값을 넣어주는 동작이 콜백 형태로 실행된다.

// 예시 : 배열의 각 요소를 두 배로 곱해서 새로운 배열을 생성하는 콜백 함수 
let numbers = [1, 2, 3, 4, 5]; // 배열 선언 
let doubled = []; // 빈 배열 선언 

// numbers 배열의 각 요소에 대해 콜백 함수 실행 
numbers.forEach(function (num) { 
    doubled.push(num * 2); // 콜백 함수로 각 요소를 두 배로 곱해서 doubled 배열에 추가 
}); 

console.log(doubled); // [2, 4, 6, 8, 10]

Ajax 결과값을 받을 때 사용

서버에서 데이터를 주고받을 때 fetch 메서드에서 서버 요청 결과값을 받을때까지 기다리고 처리하기 위해 콜백 함수가 사용된다.

// fetch 메서드를 사용하여 서버로부터 JSON 데이터를 받아오고 콜백 함수로 화면에 출력
fetch("https://jsonplaceholder.typicode.com/users")
  .then(function (response) {
    // fetch 메서드가 성공하면 콜백 함수로 response 인자를 받음
    return response.json(); // response 객체의 json 메서드를 호출하여 JSON 데이터를 반환
  })
  .then(function (data) {
    // json 메서드가 성공하면 콜백 함수로 data 인자를 받음
	console.log(data);
  })

콜백 함수를 통한 비동기 처리 방식 문제 해결

아래 코드에서 콘솔 로그 결과는 undefined이다. 이유는 자바스크립트의 비동기 처리 방식으로 $.get으로 요청한 데이터를 tableData에 저장하기 전에 리턴을 수행하기 떄문이다.

function getData() {
	var tableData;
	$.get('https://domain.com/products/1', function(response) {
		tableData = response;
	});
	return tableData;
}

console.log(getData()); // undefined

해당 문제는 콜백 함수를 이용함으로써 해결할 수 있다. 특정 로직이 끝나면 콜백 함수를 호출하여 데이터를 전달하는 방식이다.

function getData(callbackFunc) {
	$.get('https://domain.com/products/1', function(response) {
		callbackFunc(response); // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
	});
}

getData(function(tableData) {
	console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});

콜백 함수를 활용하여 중첩 데이터를 관리하는 경우 Callback hell에 빠질 위험이 있다. 이는 가독성도 좋지않고 로직을 변경하기도 어려워서 코드 작성시 유의해야 한다.

$.get('url', function(response) {
	parseValue(response, function(id) {
		auth(id, function(result) {
			display(result, function(text) {
				console.log(text);
			});
		});
	});
});

Callback hell을 해결하는 방법은 아래와 같다.

function parseValueDone(id) {
	auth(id, authDone);
}
function authDone(result) {
	display(result, displayDone);
}
function displayDone(text) {
	console.log(text);
}
$.get('url', function(response) {
	parseValue(response, parseValueDone);
});

중첩하여 선언한 콜백 익명 함수를 각각의 함수로 구분하여 함수 내 메서드를 통해 호출하면 된다. (각 메서드의 기능은 생략됨)

PROMISE

콜백 함수를 사용하다보면 Callback hell에 빠지는 경우가 발생한다. 아래 1초마다 1씩 더해서 출력하는 setTimeout 비동기 함수로 구현한 코드를 살펴보자

function increaseAndPrint(n, callback) {
  setTimeout(() => {
    const increased = n + 1;
    console.log(increased);
    if (callback) {
      callback(increased); // 콜백함수 호출
    }
  }, 1000);
}

increaseAndPrint(0, n => {
  increaseAndPrint(n, n => {
    increaseAndPrint(n, n => {
      increaseAndPrint(n, n => {
        increaseAndPrint(n, n => {
          console.log('끝!');
        });
      });
    });
  });
});

위 코드는 가독성도 떨어지고 흐름을 파악하기에도 어려워진다. 또한 콜백 함수마다 에러 처리를 따로 해줘야하고 에러가 발생한 위치를 찾기도 어려워진다.

PROMISE로 개선된 비동기 처리 문법

이를 자바스크립트 PROMISE로 리펙토링하면 중첩 구문이 사라지고 코드 흐름을 파악하기 수월해진다. .then()을 통해 이들을 열거하여 비동기 처리 결과를 깔끔하게 표현한다.

function increaseAndPrint(n) {
  return new Promise((resolve, reject)=>{
    setTimeout(() => {
      const increased = n + 1;
      console.log(increased);
      resolve(increased);
    }, 1000)
  })
}

increaseAndPrint(0)
  .then((n) => increaseAndPrint(n))
  .then((n) => increaseAndPrint(n))
  .then((n) => increaseAndPrint(n))
  .then((n) => increaseAndPrint(n)); // 체이닝 기법

PROMISE 객체 기본 사용법

PROMISE 객체는 자바스크립트의 Array, Object 처럼 독자적인 객체이다. 비동기 작업이 끝날때까지 기다리는것이 아니라 결과를 제공하겠다는 '약속'을 반환한다는 의미에서 PROMISE라고 지어졌다고 한다.

프로미스 객체 생성

new 키워드와 Promise 생성자 함수를 사용하여 Promise 객체를 생성한다 이때 두개의 매개변수를 사용하는데 첫번째 인수는 성공(resolve)했을 때 성공 임을 알려주는 객체이고 두번째 인수는 작업이 실패(reject)했을 때 실패임을 알려주는 오류 객쳉이다.

const myPromise = new Promise((resolve, reject) => {
	// 비동기 작업 수행
    const data = fetch('서버로부터 요청할 URL');
    
    if(data)
    	resolve(data); // 만일 요청이 성공하여 데이터가 있다면
    else
    	reject("Error"); // 만일 요청이 실패하여 데이터가 없다면
})

프로미스 객체 처리

Promise 객체는 비동기 작업이 완료된 이후 다음 작업을 연결시켜 진행할 수 있다. 작업 결과에 따라 then(), catch() 메서드 체이닝을 통해 성공과 실패에 대한 후속 처리를 진행할 수 있다. 처리가 정상적으로 수행되면 resolve(data)를 호출하게 되면 바로 .then()으로 이어져 then 메서드의 콜백 함수에서 성공에 대한 추가 처리를 진행한다. 이때 호출한 resolve() 함수의 매개변수 값이 then 메서드의 콜백 함수 인자로 들어가 then 메서드 내부에서 미로미스 객체 내부에서 다룬 값을 사용할 수 있게된다.


myPromise
    .then((value) => { // 성공적으로 수행했을 때 실행될 코드
    	console.log("Data: ", value); // 위에서 return resolve(data)의 data값이 출력된다
    })
    .catch((error) => { // 실패했을 때 실행될 코드
     	console.error(error); // 위에서 return reject("Error")의 "Error"가 출력된다
    })
    .finally(() => { // 성공하든 실패하든 무조건 실행될 코드
    	
    })

.then 구문에 익명 함수를 입력하고 value 매개변수를 전달하면 resolve는 해당 콜백 함수를 호출하면서 호출에 성공한 값을 넣어주는 형태이다. 반면 error가 발생하는 경우 error에 해당하는 객체를 넣어준다.

PROMISE 함수 등록

위와 같이 객체를 변수에 바로 할당하는 방식이 아닌 아래 방식을 사용하는 것이 일반적이다.

// 프로미스 객체를 반환하는 함수 생성
function myPromise() {
  return new Promise((resolve, reject) => {
    if (/* 성공 조건 */) {
      resolve(/* 결과 값 */);
    } else {
      reject(/* 에러 값 */);
    }
  });
}

// 프로미스 객체를 반환하는 함수 사용
myPromise()
    .then((result) => {
      // 성공 시 실행할 콜백 함수
    })
    .catch((error) => {
      // 실패 시 실행할 콜백 함수
    });

위와같이 함수를 만들고 호출하는 방식으로 return으로 PROMISE를 반환하여 사용하는 것이 일반적이다. 이렇게 사용하는 경우 아래와 같은 이점이 존재한다.
1. 재사용성 : 함수로 만든 프로미스 함수를 필요할때마다 호출하여 반복되는 비동기 작업을 효율적으로 처리할 수 있다.
2. 가독성 : 프로미스를 함수로 생성할경우 코드의 구조가 명확해져서 코드의 가독성을 높일 수 있다.
3. 확장성 : 함수로 만들어진 프로미스 객체에 인자를 전달하여 동적으로 비동기 작업을 수행할 수 있으며 여러개의 프로미스 객체를 반환하는 함수들을 연결하여 복잡한 비동기 로직을 구현할 수 있다.

이러한 방식을 프로미스 팩토리 함수라고 불리운다.

이러한 프로미스 팩토리 함수 방식을 사용한 자바스크립트 메서드는 fetch() 메서드이다. 해당 메서드 내에서 프로미스 객체를 생성하여 서버로부터 데이터를 가져오면 resolve()하여 then()으로 처리하기 때문이다.

// GET 요청 예시
fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then((response) => response.json()) // 응답 객체에서 JSON 데이터를 추출한다.
  .then((data) => console.log(data)); // JSON 데이터를 콘솔에 출력한다.

PROMISE 3가지 상태

new Promise() 생성자로 프로미스 객체를 생성하면 그 비동기 작업은 이미 진행중이고 언젠가는 성공하거나 실패할 것이다. 이러한 상태를 프로미스의 상태(state)라고 불리운다.
1. Pending : 처리가 완료되지 않은 상태 (처리 진행중)
2. Fulfilled : tjdrhdwjrdmfh cjflrk dhksfyehls tkdxo
3. Ejected : 처리가 실패로 끝난 상태

PROMISE 핸들러

  1. then : 프로미스가 fulfilled 되었을 때 실행할 콜백 함수를 등록하고 새로운 프로미스를 반환한다.
  2. catch : 프로미스가 rejected 되었을 때 실행할 콜백 함수를 등록하고 새로운 프로미스를 반환
  3. finally : 프로미스 fulfilled, rejected 여부와 관계없이 실행할 콜백 함수를 등록하고 새로운 프로미스를 반환한다.

PROMISE 체이닝

프로미스 체이닝이란 프로미스 핸들러를 연달아 연결하는 것을 의미한다. 이렇게하면 여러개의 비동기 작업을 순차적으로 수행할 수 있다는 특징이 있다. doSomething 함수를 호출하여 프로미스를 생성하고 then 메서드를 통해 이행 핸들러를 연결하는 과정을 보여준다. 각 이행 핸들러는 이전 프로미스의 값에 50을 더한 값을 반환하고 마지막 이행 핸들러는 최종 값을 콘솔에 출력하게 된다.

function doSomething() {
  return new Promise((resolve, reject) => {
      resolve(100)
  });
}

doSomething()
    .then((value1) => {
        const data1 = value1 + 50;
        return data1
    })
    .then((value2) => {
        const data2 = value2 + 50;
        return data2
    })
    .then((value3) => {
        const data3 = value3 + 50;
        return data3
    })
    .then((value4) => {
        console.log(value4); // 250 출력
    })

이러한 체이닝이 가능한 이유는 then 핸들러에서 값을 리턴하면 그 반환값은 자동으로 프로미스 객체로 감싸져 반환되고 그 다음 then 핸들러에서 반환된 프로미스 객체를 받아 처리하기 때문이다. 프로미스 처리 프로세스는 아래와 같다.

AWAIT, ASYNC

AWAIT, ASYNC는 Callback hell, Promise hell로 인해 발생하는 코드 해석의 어려움, 에러 발생 시 원인 분석의 어려움 등을 간결한 문법을 제공함으로써 해결해준다.

/* Callback Hell */
getData (function (x) {
  getMoreData (x, function (y) {
    getMoreData (y, function (z) {
      ...
    });
  });
});
  
/* Promise Hell */
fetch('https://example.com/api')
  .then(response => response.json())
  .then(data => fetch(`https://example.com/api/${data.id}`))
  .then(response => response.json())
  .then(data => fetch(`https://example.com/api/${data.id}/details`))
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

Callback hell, Promise hell 모두 가독성이 좋지 않으며 실제 코드가 어떤 목적으로 작성되었는지 유추하기 어렵다. 반면 AWAIT, ASYNC 문법을 사용할 경우 마치 함수의 리턴값을 변수가 받는 정의문 형식으로 되어있어 코드가 의도하고자하는 바를 동일 코드 레벤 라인에서 알 수 있다.

async function getData() {
    const response = await fetch('https://example.com/api');
    const data = await response.json();
    const response2 = await fetch(`https://example.com/api/${data.id}`);
    const data2 = await response2.json();
    const response3 = await fetch(`https://example.com/api/${data.id}/details`);
    const data3 = await response3.json();
    console.log(data3);
}

getData();

자바스크립트 Async, Await

Async, Await는 내부적으로 PROMISE를 사용해서 비동기를 처리하고 코드 작성 부분을 프로그래머가 유지보수를 편하게 할 수 있는 방법으로 문법만 다르게 보여줄 뿐이다. 실제로 Async 리턴값을 Promise 객체를 반환한다. (그래서 Async 리턴값에 then, catch, finally 등의 키워드도 붙여서 사용할 수 있다)

자바스크립트의 class와 유사하다. 자바스크립트에서는 class가 실제로는 프로토타입으로 구성된것과 같이 AWAIT, ASYNC 또한 PROMISE로 구성된다.

기본 사용법

async와 await는 절차적 언어에서 작성하는 코드와 같이 사용법도 간단하고 이해하기 쉽다. 단순히 function 키워드 앞에async를 붙여주고 비동기 처리되는 부분 앞에 await만 붙여주면 된다.

// 프로미스 객체 반환 함수
function delay(ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`${ms} 밀리초가 지났습니다.`);
      resolve()
    }, ms);
  });
}

// 기존 Promise.then() 형식
function main() {
  delay(1000)
      .then(() => {
        return delay(2000);
      })
      .then(() => {
        return Promise.resolve('끝');
      })
      .then(result => {
        console.log(result);
      });
}

// 메인 함수 호출
main();
// async/await 방식
async function main() {
  await delay(1000);
  await delay(2000);
  const result = await Promise.resolve('끝');
  console.log(result);
}

// 메인 함수 호출
main();

async, await 에러 처리

기존 Promise의 경우 catch 핸들러를 중간 중간에 명시하여 에러를 받아야 했다.

// then 핸들러 방식
function fetchResource(url) {
  fetch(url)
    .then(res => res.json()) // 응답을 JSON으로 파싱
    .then(data => {
      // data 처리
      console.log(data);
    })
    .catch(err => {
      // 에러 처리
      console.error(err);
    });
}

async, await는 try/catch 문을 사용해서 에러를 처리한다.

// async/await 방식
async function func() {

    try {
        const res = await fetch(url); // 요청을 기다림
        const data = await res.json(); // 응답을 JSON으로 파싱
        // data 처리
        console.log(data);
    } catch (err) {
        // 에러 처리
        console.error(err);
    }

}
func();

이처럼 async/await의 장점은 비동기 코드를 마치 동기 코드처럼 읽히게 해준다는 점이다. 우리가 일반적으로 코드를 쓰고 읽어 내리는것과 같다.

Javascript Map

JAVASCRIPT MAP

RxJS

RxJS
추후 상세 정리..

1개의 댓글

comment-user-thumbnail
2023년 8월 5일

정보 감사합니다.

답글 달기