본문 바로가기
Front-end/Vanilla JS

JS 근본 공부 - 콜백(Callback)

by devraphy 2020. 11. 5.

- 콜백은 비동기 처리방식을 구현하기 위해 기본적으로 이해 해야하는 개념이다. 

- 콜백은 무엇인지, 비동기/동기처리는 무엇인지 알아보자.

 

1. Sync & Asnyc

a) Sync(동기)

- 기본적으로 JavaScript는 동기적(synchronous)으로 작동하는 언어이다.

- 이는 코드를 읽을 때, 자바스크립트의 hoisting 과정 이후 코드가 작성된 순서대로 실행된다는 의미다. 

- 즉, 순서대로 작동한다는 것은 동기적으로 처리된다는 뜻이다. 

 

* hoisting: var 변수, function의 선언부가 몇번째 줄에 작성 되었는지와 상관없이 가장 먼저 읽혀 수행되는 것

 

 

b) Async(비동기)

- 동기적 처리와 반대되는 개념으로, 코드의 작성 순서와 상관없이 개발자가 순서를 지정하여 동작하는 것을 의미한다. 

- 간단한 예를 통해 확인해보자. 이 개념을 확인해보자. 

"use strict";

console.log("1");
setTimeout(function () {
  console.log("2");
}, 1000);
console.log("3");

- 위의 코드에서 setTimeout 함수는 브라우저가 제공하는 API로, 설정한 ms(밀리 세컨드 1/1000초)가 지나면 내부에 작성한 콜백함수를 호출하는 함수이다.

- JavaScript는 동기적으로 처리하는 언어이므로 위의 코드를 수행하면 1이 출력된 후 1초뒤에 2가 출력되고 3이 출력될 것이다. 정말 그럴까? 

 

* 콜백함수 - 특정 조건에 의해 호출되도록 미리 정의해 놓은 함수

* setTimeOut(콜백함수, 시간);

 

 

[출력결과]

- 하지만 출력결과는 추측과는 다르게 1, 3, 2가 나온다. 왜 그럴까? 

- 동작과정을 살펴보자. 

 

 

[동작순서]

1. hoisting

2. console.log("1"); 실행

3. setTimeout(); 실행 및 1초 카운트 시작(비동기)

4. console.log("3"); 실행 (브라우저가 전달된 연산을 처리하는 동안)

5. 1초가 지난 후 브라우저에 의해 콜백함수 호출 → console.log("2"); 실행 

 

- 이처럼 코드의 작성순서와 상관없이 연산이 수행되는 것비동기적 처리라고 한다. 

 

c) 콜백은 항상 비동기적인 처리에서만 사용되는 것일까? 

- 그렇지 않다. 콜백은 재귀함수의 형태이기 때문에 비동기적/동기적 처리 모두에서 사용된다. 

 

 

[Synchronous callback 예시]

"use strict";

console.log("1");
setTimeout(function () {
  console.log("2");
}, 1000);
console.log("3");

// Synchronous callback
function printImmediately(print) {
  print();
}

printImmediately(() => console.log("printImmediately()"));

 

 

[출력결과]

 

- 위의 코드가 실행될 때, 어떤 순서로 작동했을지 이제는 감이 올거라고 생각한다.

- 작동 순서는 다음과 같다. 

 

 

[동작순서]

1. hoisting → printImmediately 함수의 선언문 

2. console.log("1"); 실행

3. setTimeout(); 실행 및 1초 카운트 시작(비동기)

4. console.log("3"); 실행

5. printImmediately(); 실행(동기)

6. 1초가 지난 후  콜백함수 호출 → console.log("2"); 실행

 

- 왜 printImmediately() 함수가 더 먼저 실행되냐 라고 묻는다면, setTimeout() 함수의 콜백함수가 실행 되기까지 걸리는 1초의 시간보다 그 이후 코드들의 연산하는데 걸리는 시간이 더 빨라서이다. 

 

 

[Asynchronous callback 예시]

 

- 기존의 코드에 이어서 아래의 printWithDelay 부분의 코드를 작성하기 바랍니다. 

"use strict";

console.log("1");
setTimeout(function () {
  console.log("2");
}, 1000);
console.log("3");

// Synchronous callback
function printImmediately(print) {
  print();
}

printImmediately(() => console.log("printImmediately()"));

// Asynchronous callback
function printWithDelay(print, timeOut) {
  setTimeout(print, timeOut);
}

printWithDelay(() => console.log("Async callback"), 2000);

 

 

[출력결과]

- 이번에는 비동기식 처리를 사용하는 함수를 작성해보았다. 어떤 과정을 통해서 이와 같은 출력결과를 만들어 냈을까?

- 그 과정은 다음과 같다. 

 

 

[동작순서]

1. hoisting → printImmediately()와 printWithDelay() 함수의 선언문

2. console.log("1") 실행 

3. setTimeout() 실행 및 1초 카운트 시작(비동기)

4. console.log("3") 실행 

5. printImmediately() 실행(동기)

6. 1초가 지난 후  콜백함수 호출 → console.log("2") 실행

7. printWithDelay() 실행 및 2초 카운트 시작(비동기)

7. 2초가 지난 후 콜백함수 호출 → console.log("Async callback") 실행

 

 


2. Callback 지옥

- 콜백을 처음 사용하다 보면 콜백에서 빠져나올 수 없는 경우가 종종 발생하곤 한다. 이런 경우를 콜백 지옥이라고 부른다. 

- 콜백 지옥이 실제로 어떻게 코드로 구현되고 어떤식으로 동작하는지 예시를 통해 확인해보자.

 

 

[콜백지옥 예시]

- 사용자가 로그인을 하면 사용자의 데이터를 서버에서 받아오는 시나리오

- 프로그램의 구조적 관점으로 봤을 때, 좋은 예시가 아니기에 콜백지옥의 개념만 이해하자.

class UserStorage {
  loginUser(id, password, onSuccess, onError) {
    // 로그인 검증 함수
    // 로그인 성공시 onSuccess 콜백 함수 호출
    // 로그인 실패시 또는 에러발생시 onError 콜백 함수 호출
    setTimeout(() => {
      if (
        // 로그인 유효성 검증을 간단한 코드로 구현
        // 아래 id와 password에 포함되지 않은 경우 에러가 발생하는 시나리오
        (id === "kevin" && password === "kevin123") ||
        (id === "admin" && password === "master")
      ) {
        onSuccess(id);
      } else {
        onError(new Error("not found"));
        // 에러 발생을 위한 에러 객체 생성
      }
    }, 2000); // 로그인 과정을 가짜로 구현하기 위한 2초의 시간 delay
  }

  getRoles(user, onSuccess, onError) {
    // 사용자 마다의 역할/권한 정보를 받아오는 함수
    setTimeout(() => {
      if (user === "kevin") {
        onSuccess({ name: "kevin", role: "developer" });
      } else {
        onError(new Error("not found"));
      }
    }, 1000);
  }
}

 

 

[동작순서]

1. 사용자로부터 id와 password를 입력받아 로그인을 시도한다.

2. 로그인에 성공한 사용자의 id를 받아와서 role을 받아온다.

3. 위의 과정을 성공하면, 해당 사용자의 이름과 role을 받아온다. 

 

- 이제 시나리오에 필요한 시스템의 구조를 구현했으니, 실제 동작하는 부분을 구현해보자. 

 

 

[콜백지옥 동작부분 예시]

const userStorage = new UserStorage();
const id = prompt("enter your id: ");
const password = prompt("enter your password: ");
userStorage.loginUser(
  id,
  password,
  (user) => {
    userStorage.getRoles(
      user,
      (userWithRole) => {
        alert(
          `Hello, ${userWithRole.name}. You have a ${userWithRole.role} role.`
        );
      },
      (error) => {
        console.log(error);
      }
    );
  },
  (error) => {
    console.log(error);
  }
);

 

 

[동작순서]

1. prompt를 통해 사용자로부터 id와 password를 입력받는다.

2. 입력된 id와 password를 이용하여 login을 수행한다. (loginUser함수 실행)

3. 로그인이 실패한 경우 error를 출력하고, 성공한 경우 getRole 함수를 실행한다. 

4. getRole 함수를 성공적으로 실행한 경우 로그인한 사용자의 이름과 직책이 출력되고, 실패한 경우 error를 출력한다.

 

 

- 실제로 코드를 실행시켰을 때, 어떤 결과가 나올까? 

 

 

 

[출력결과]

 

- kevin을 입력한 경우, 팝업창이 뜨면서 결과가 잘 출력된다. 

 

 

- kevin이외의 것을 입력한 경우, 작성한 not found가 잘 출력된다. 

 

 

a) 결과는 작성한대로 잘 출력된다. 여기서 무엇이 문제일까?

 

1. 코드의 가독성이 최악이라는 점

- 그냥 읽고 이해하기 불편하다. 어디서 어디가 연결되어 있는지 파악하기 어렵다.

- 이는 추후에 유지보수를 할 때, 구조를 분석하는데 굉장히 복잡하고 시간이 오래걸리게 된다.  

 

 

2. 콜백 함수가 서로 맞물려 있다는 점

- 함수 안에서 함수를 호출하는 방식이기 때문에 과정이 많을수록 복잡한 콜백 구조를 이룬다.

- 그로인해 하나의 함수를 고쳐도 전체 프로그램에 영향을 미치게된다.

- 이는 유지보수의 방해요소가 되며, 동시에 코드의 효율성(시간복잡도)도 굉장히 떨어진다. 

 

 

 

 

 

[정보출처]

 

www.youtube.com/watch?v=s1vpVCrT8f4&t=8s

 

'Front-end > Vanilla JS' 카테고리의 다른 글

JS 근본 공부 - async & await  (0) 2020.11.09
JS 근본 공부 - Promise  (2) 2020.11.06
JS 근본 공부 - JSON  (0) 2020.11.04
JS 근본 공부 - 배열 API 연습문제  (0) 2020.11.03
JS 근본 공부 - 배열(Array)  (0) 2020.11.02

댓글