Callback - 비동기 처리의 시작

DatQueue·2022년 3월 16일
1
post-thumbnail

callback function 시작하기

작성자 본인의 첫 velog ( 실제적 내용 관련 ) 포스팅에는 Javascript 비동기 처리의 시발점이라고 할 수 있는 callback function (콜백 함수)에 관해 얘기해보고자 한다.
"콜백 함수" , "콜백 지옥" 등등 용어는 많이 들어보고 심지어 콜백 함수를 자연스럽게 많이 써본 경험도 있을 것이다. 물론 여러 훌륭한 개발자분들이나 자바스크립트에 능숙하신 분들께는 오늘 얘기해나갈 내용들이 되게 기본적이고 시시할 수 있다. 하지만 본인과 같이 이 부분을 처음 접하는 걸음마 단계에선 바로 그 "기본" 만큼 중요한 것이 없다고 본다.

Javascript에서 "callback"이란 어떤 의미이고, 어떻게 쓰이고, 왜 필요한지에 대해 이번 포스팅에서 다루고자 한다. 이런 원천적인 개념이 잘 깔려있어야 Promise 함수에도 잘 다가갈 수 있다고 생각한다.

참고 지식 - first class citizen

콜백함수에 진입하기 앞서 "first class citizen" 이란 용어에 대해 알고 넘어갈 필요가 있다. 물론 이 개념이 없다고 해서 callback함수를 이해하는데 전혀(?) 지장이 없긴하다. 그렇지만 알아두어서 전혀 나쁠 것이 없기 때문에 작성해보고자 한다.

잠깐 프로그래밍과 관련없는 얘기를 해본다. "first class citizen" 은 프로그래밍에선 "1급 객체" 라는 의미를 가진다. 그렇지만 영어 그대로 번역시 모두가 한번쯤 들어보았을 단어인 "1급 시민" 이다. "first class citizen (일급 시민)" 이란 자유롭게 거주하고 일을 할 수 있고, 국가간의 출입국에 있어 자유를 가지며, 투표의 자유또한 가질 수 있는 자유 시민을 의미한다. 예상했겠지만 "second class citizen (2급 시민)" 또한 존재한다. "second class citizen"은 시민 또는 합법적 거주자이지만 시민권 및 사회 경제적 기회가 제한되어있는 시민을 의미한다.

이것을 프로그래밍 세계에 옮겨온 것이라 생각하면 편하다.

프로그래밍 언어 디자인에서, 특정 언어의 first class citizens( first-class type or first-class object or first-class value라고도 부름) 이란 보통 다른 객체들에게 적용 가능한 연산을 모두 지원하는 객체를 말한다.

즉, 프로그래밍 언어에서 type을 전달, 반환 및 할당 할 수 있는 경우 해당 type을 1급 객체로 간주한다. 그럼 우리가 공부하는 언어인 "Javascript"에서의 "first class citizen"에 대해 알아보자.

first-class citizen의 조건은 다음과 같다.

  • 변수나 데이터 구조안에 담을 수 있다.
  • parameter값으로 전달 할 수 있다.
  • return값으로 사용할 수 있다.

다음 예시를 살펴보자.

const a = 1;

당연하겠지만 1이라는 "숫자"는 변수 a에 할당될 수 있다. 또한 그 a란 값을 어떠한 컴포넌트로 전달에 return하는 것 또한 가능하다. 즉 "숫자" 라는 type은 "first-class citizen"인 것이다.

이어서 조건문( if 문 )의 경우엔 어떨까?

const b = if(~~){
	~~~;
}

누가 봐도 이상하다. 저런 구문은 본 적도 없고 말도 안된다. 물론, 함수안의 조건문으로써는 쓰일 수 있다. 그렇지만, 저렇게 if 문 단독적으로 어떤 변수에 할당되는 것은 맞지 않다.
이럴 경우엔 "second-class citizen"이라고 한다.

마지막으로 함수(function)의 경우로 살펴보자.

const c = function(~~){
	return ~~;
}

보통 우리는 위와 같은 형식을 "함수 표현식" 이라고 부르기도 하며 ( 함수 표현식에 대해선 추후에 다루겠다.) 전혀 어색할 것 없는 구문이다.
사실 함수를 어떤 변수에 담아서 사용할 수 있다는 점이 바로 Javascript만의 매력이 아닐까 싶다.

변수로 담을 수 있다는 것 외에 first class citizen에 해당하는 타입은 return값으로 쓰일 수 있어야 한다고 나와있다. 그 점 또한 만족해야하므로 확인해보자.

function fn(){
	const c = function(~){
    	return ~;
    }
    return c;  // 함수를 할당한 변수 c를 fn()함수에서 return하였음
}

전혀 이상이 없는 구문이다.

마지막 조건인 parameter값으로도 쓰일 수 있을까? 물론, 당연하다.

function fn(){
  const d = function(){
    return ~ ;
  }
}

fn(d); // 함수를 할당한 변수 d가 감싸고 있는 함수 fn()의 parameter로 넘겨졌다.

자, 이제 Javascript는 "함수"를 "first-class citizen"으로 인정한 것이다.

callback 함수란 무엇일까?

callback 함수는 간단하게 말하자면 parameter로 넘겨준 함수를 말한다.
parameter로 넘겨받은 함수는 일단 넘겨받은 후, 때가 되면 나중에 호출(called back)한다는 것이 callback 함수의 개념이라 할 수 있다.

코드를 보면 더 이해가 쉬울 것이다.

const fn1 = function () {   // 함수 표현식
  console.log("hello");
};

function fn2(arg) {        // 일반 함수식
  arg();
}

fn2(fn1);

함수 fn1과 fn2이 정의되어있고, 마지막에 fn2함수를 호출하였는데 이때 parameter가 fn1인 것을 알 수 있다. 이 때 fn2함수 식을 보면 parameter인 arg를 함수내부에서 호출하였다. 마지막에 fn2의 parameter로 받은 fn1함수 자체가 arg인 것이고, fn1함수를 fn2안에서 실행한 셈이다.

위에 언급하였던 "일단 넘겨받은 후, 때가 되면 나중에 호출" 이란 말이 바로 이 점이다. 함수 fn1을 일단 fn2안에 넘겨받은 후, 때가되면 fn2 안에서 호출한 것이다.

결과는 뭐 당연히 fn1이 호출되므로 fn1함수안의 실행값이 출력된다.

callback 함수의 정의에 대해선 더욱 깊게 설명할 것도 없다. 위 내용이 정의의 전부이고 베이스로 삼아서 다음 내용을 진행하고자 한다.

callback 함수는 왜 (why?) 필요할까?

callback 함수가 왜 필요할지에 대해 생각하기 전, 앞전과 비슷한 콜백 예시를 보자.

const fn1 = function () {   
  console.log("2");
};

function fn2(arg) {
  console.log("1");           //추가
  arg();
}

fn2(fn1);

결과를 확인해보면 다음과 같다.

fn2함수안의 "1"가 먼저 출력이되고, 그 뒤에 parameter로 받은 fn1함수("2")가 출력되게 된다.
콜백으로써 작동한 것이다.

그런데 생각을 해보면

function fn1() {
  console.log("1");
}
function fn2() {
  console.log("2");
}

fn1();
fn2();

이렇게 간단하게 fn1함수를 작성하고, fn2함수를 작성하고 난 뒤 fn1,fn2를 순서대로 호출하게 된다면 1,2가 순차적으로 호출된다. 위에 콜백예시보다 훨씬 쉽게 1,2를 순차적으로 호출할 수 있다. 아마 이 간단한 코드를 보고 "첫 번째 예시가 더 쉬운데?"라고 생각하는 미친? 사람은 없을 것이다.

이렇게 동기적으로 코드를 짜는 방법이 더 직관적이고 간단한데 왜 우린 callback함수를 사용하는 것일까?

물론, 두 번째 (바로 위의 코드) 처럼 코드 진행에 있어 문제가 없고 원하는 값을 순차적으로 도출해 낼 수 있다면 callback함수는 불필요함에 틀림없다. 하지만 만약 예시의 fn1함수가 비동기처리가 되는 함수였다면 어떨까? 말하자면, fn1함수가 비동기처리로 인해 나중에 다른 코드가 진행된 후 호출되는 것이다. 그렇게 된다면 순차적 값을 얻지 못하게 된다.

한번 fn1함수를 비동기 처리 시켜보자.
실제로 Ajax통신을 이용해 데이터를 불러오는 작업과 같은 hard한 비동기 통신을 수행하긴 어려우므로 최대한 비동기적이게끔 setTimeout API를 이용하겠다.

setTimeout()-Web API | MDN

function fn1() {
  setTimeout(() => {      // use setTimeout 
    console.log("1");
  }, 1000);
}
function fn2() {
  console.log("2");
}

fn1();
fn2();

결과를 확인해보자.

2가 먼저 출력되고 1000ms (1초) 뒤 1이 출력된다. 분명 fn1을 먼저 호출했건만 fn2안의 2가 먼저 출력된 모습이다.

어떻게 진행된 것일까?

fn1을 호출하는 fn1( );은 얼핏 생각하면 fn1함수 내부의 setTimeout( )과 그 내부에 있는 console.log("1"); 까지 전부 호출하는 것으로 생각할 수도 있다. 하지만 fn1( );은 setTimeout을 예약만 걸어주는 행위 그 이상을 진행하지 않는다.
즉, fn1( );이 fn2( )보다 먼저 실행되는 것은 맞지만 그것은 setTimeout( )을 예약만 걸어두는 행위에 불과한 것이다. 결국 뒤의 코드들이 실행된 후 1초뒤 setTimeout( ) 내부에 있는 console.log("1")을 호출하게 된다.

이렇게 동기적으로 코드를 짜게 되었을 때, 먼저 작성된 함수가 비동기적일 경우 순차적 실행이 불가하다. 만약 우리가 무조건적으로 1이 먼저 출력되고 그 후 2가 출력되는 코드를 작성하고 싶다면 이런 동기적코드는 항상 옳지 않다는 것이다.

여기서 우린 callback함수의 존재 이유, 필요성을 알게 된 셈이다.
조금 극단적으로 말하자면 callback함수의 용도는 " 순차적으로 실행하고 싶을 때 씀 " 그 이상 그 이하도 아니다.

callback함수의 재사용성

위의 언급한 내용과 이어지는 연장선상에서 생각할 수 있는데, callback함수를 사용하게 되면 가독성은 물론이고 코드 재사용성 면에서 굉장히 유용하다.

만약, 앞선 코드를 예시로 들때, 여러 개발자 팀원들이 fn1( )함수를 너무 유용하게 써서 필요할 때마다 가져다 쓰는 코드가 되었다고 가정하자. 다시 말하자면 "console.log(1);"을 호출하는 함수를 여러 팀원들이 필요로 한다는 것이다.

이때, A팀원은 1을 호출한 뒤, 바로 다음에 2를 호출하고 싶다고 가정해보자.

그렇다면 이처럼

function fn1(parameter) {
  console.log(1);
  parameter();
}

fn1(function () {
  console.log(2);
});

fn1 함수에 parameter만 받아주고 그 parameter를 console.log(2);를 호출하는 "함수"로 만들어 fn1( )안에 넣어 호출하면 된다.

만약 B팀원은 1을 호출한 뒤, 바로 다음에 3을 호출하고 싶다면 위와 같은 형식에

fn1(function(){
  console.log(3);
});

다음과 같이 작성해주면 되는 것이다.

이처럼 callback함수는 가독성재사용이 용이하게 된다.

Callback Hell (콜백 지옥)은 무엇일까?

위에서 잠깐 언급했다시피 callback함수는 보통 json을 활용한 Ajax 요청 ( 나중에 따로 다루겠다 . 참고만 하자. ) 및 DB에서 데이터를 불러오는 과정에 많이 이용된다.

한 가지 예로 만약 DB에서 데이터를 뽑고 싶은데 A를 뽑고 그 다음 B를 뽑고, 그 다음 C를 뽑는 순으로 진행하고 싶다 가정하자.

db.collection('post').findOne(A,()=>{
  db.collection('post').findOne(B,()=>{
    db.collection('post').findOne(C,()=>{
      ...
    })
  })
})

대충 이런 느낌의 calllback함수 코드가 짜여질 것이다.

불러오는 데이터가 A,B,C에서 끝나면 다행이겠지만 점점 더 뽑고 싶은 데이터가 늘어나고 알파벳 순으로 (순차적으로) 데이터를 불러와야한다면 callback함수 코드는 끊임없이 오른쪽으로 옆구리 터진것마냥? 움직일 것이다.

이것이 바로 그 유명한 Callback Hell이다.

이 "콜백 지옥"을 벗어나기 위한 방안으로 Promise, async-await 이 나오게 된다. 이러한 비동기 처리 함수들은 앞으로 나올 포스팅에 이어서 다룰 예정이다.

콜백 지옥에 대해 너무 짧게 다룬 점이 없지않아 있지만 이 "콜백 지옥"을 더욱 잘 느끼기 위해서는 Promise기반의 함수와 비교하면서 다루는 것이 좋기 때문에, 개념 기반 포스팅이 끝난 후 따로 다룰 예정이다.

마무리 ....

지금까지 예제라고도 할 수 없는 아주 간단한 예제들을 통해 callback함수가 무엇이고, 어떻게 쓰이고, 왜 필요한지에 대해 알아보았다. 누군가에겐 아주 단순한 개념일지도 모르지만 작성자 본인과 같이 callback함수의 작성법을 먼저 배우면서 겪었던 어려움을 가지고 있는 분들께는 이러한 내용이 꼭 필요할 것이라 생각이 든다.

callback함수에 대해 여러 예제들을 더 다루고 싶지만 내용이 너무 길어질 것 같으므로 아주 짧은 포스팅으로 마무리 짓고자 한다.
그럼 다음 포스팅에서는 callback 함수의 몇 가지 "사례" 들에 대해 작성해보고 생각을 정리해볼 예정이다.

이상.

( 참고로, Promise를 먼저 정리 후 callback을 뒤늦게 포스팅하는 바람에 순서가 뒤바꼇네요,,, ㅠㅠㅠ 이전 포스팅에 Promise가 나오지만 시리즈 순서를 참고해주서 봐주시면 감사하겟습니다.)

profile
You better cool it off before you burn it out / 티스토리(Kotlin, Android): https://nemoo-dev.tistory.com

0개의 댓글