관련지식
javascript, node.js, polyfill, callback, Promise, async/await

비동기 함수 test1() 과 test2() 를 만들어야 합니다. test1() 함수의 호출이 끝난 후에 test2() 함수가 실행되어야 할때, 어떤 형태로 호출할수 있도록 만드는게 좋을까요?

콜백 함수 형태

  1. test1(function(v1) {
  2. console.log(v1);
  3. test2(function(v2) {
  4. console.log(v2);
  5. console.timeEnd();
  6. })
  7. });

첫번째로 가장 기본적인 콜백함수 형태로 호출할수 있겠네요. 이것은 모든 브라우저에서 동작 가능하기 때문에 여전히 많이 쓰이는 구조 입니다. 하지만 콜백함수 호출이 많아질수록 유지보수가 어려워지는 콜백 지옥을 보게 됩니다.

Promise 형태

  1. test1().then(function(v1) {
  2. console.log(v1);
  3. return test2();
  4. }).then(function(v2) {
  5. console.log(v2);
  6. console.timeEnd();
  7. });

test1()test2() 함수에서 Promise 객체를 반환하도록 만들수도 있습니다. Promise 는 콜백함수에서 어려웠던 비동기 함수의 호출 흐름을 제어하기가 용이하기 때문에 콜백함수 구조보다는 유지보수가 좋을것 같습니다. 그러나 Promise는 모든 브라우저가 지원하지 않습니다. 만약 인터넷 익스플로러에서도 동작되어야 한다면 Polyfill을 적용해야 합니다. 폴리필을 적용 하더라도 완벽하게 지원하지 못할수 있습니다.

async/await 형태

  1. let aa = await test1();
  2. let bb = await test2();
  3. console.log(aa, bb);
  4. console.timeEnd();

비동기 함수가 순서대로 호출되어야 한다면, 로직 흐름은 사실상 동기함수 2개가 차례대로 호출되는 것과 같습니다. async/await 를 사용하게 되면 비동기 함수를 호출하는 것이지만 마치 동기함수를 호출하는것 같은 착각을 하게 됩니다. 물론 복잡도는 가장 낮습니다. 다만 async/await 는 객체가 아니라 키워드이기 때문에 폴리필도 존재하지 않는다는 것입니다. 즉 인터넷 익스플로러에서는 사용할수 없습니다.

어떤 호출 방식이 가장 좋은가?

정답은 없습니다. 최신의 엔진 버전을 보장하는 Node.js 환경이라면 async/await 가 좋겠지만, 하위 브라우저를 지원해야 한다면 콜백 구조가 좋을것입니다. 모던 브라우저만 지원한다면 Promise 도 좋겠죠. 그런데 내가 만들어야 하는 함수가 어떤 환경에서 사용될지 모른다면, 모든 환경을 지원하고 싶다면 어떻게 만들어야 할까요? 즉 함수를 호출하는쪽에서 환경에 맞게 호출하는 것입니다.

Promise 함수 만들기

먼저 Promise를 지원하는 형태의 함수를 만들겠습니다.

  1. function test1() {
  2. return new Promise(function(resolve, reject) {
  3. setTimeout(function() {
  4. resolve('test1');
  5. }, 2000);
  6. });
  7. }

만약 Promise 를 사용해본적이 없다면 그에 해당하는 내용을 먼저 보시는것이 좋습니다. 제가 이전에 ‘Promise 가 뭔가요?’ 라는 글을 쓴적이 있으니 참고해보시면 좋을것 같습니다.

위 함수는 test1() 로 호출시 Promise 객체를 리턴합니다. 왜 아래처럼 만들지 않았을까요?

  1. var test1 = new Promise(function(resolve, reject) {
  2. setTimeout(function() {
  3. resolve('test1');
  4. }, 2000);
  5. });

위 방식은 test1 자체가 Promise 객체이므로 호출이 좀더 간단해지지만, 이것은 Promise를 지원하지 않는 환경에서 대응할수가 없습니다. 따라서 함수 안에서 Promise 객체를 만들어 리턴하는것이 좋습니다. test2 함수도 동일하게 만들면 아래와 같이 실행할 수 있습니다.

  1. test1().then(function(v1) {
  2. console.log(v1);
  3. return test2();
  4. }).then(function(v2) {
  5. console.log(v2);
  6. console.timeEnd();
  7. });

위와 소스로 호출이 가능했다면 아래 소스로도 호출할 수 있습니다. async/await 가 Promise를 이용하기 때문입니다.

  1. (async function() {
  2. let aa = await test1();
  3. let bb = await test2();
  4. console.log(aa, bb);
  5. console.timeEnd();
  6. })();

콜백 구조 적용하기

이번엔 콜백구조를 적용할 차례입니다. 콜백 함수를 파라미터로 받아야겠죠?

  1. function test1(cb) { //파라미터 추가

함수를 호출하는 쪽에서 콜백함수를 지정했다는 것은, 콜백함수를 쓰겠다는 의미일 것입니다. 조건에 따라 콜백함수로 동작하거나 Promise 로 동작해야 합니다. 저는 아래와 같이 동작하도록 했습니다.

Promise 사용 가능Promise 사용 불가능
콜백함수 지정콜백함수로 동작콜백함수로 동작
콜백함수 미지정Promise로 동작오류

먼저 콜백함수 조건에 대해 만듭니다.

  1. if(cb) { //콜백함수가 지정되었을때
  2. setTimeout(function() {
  3. cb('test1');
  4. }, 2000);
  5. }

Promise 조건을 추가합니다.

  1. else if(window.Promise) { //Promise가 사용가능할때
  2. return new Promise(function(resolve, reject) {
  3. setTimeout(function() {
  4. resolve('test1');
  5. }, 2000);
  6. });
  7. }

그러나 위 소스는 수정이 필요합니다. window 라는 객체는 브라우저 환경에서만 존재하는 전역객체로써 Node.js 환경에는 없는 객체이기 때문입니다. 따라서 아래처럼 변경해야 합니다.

  1. var global = this;
  2. ...
  3. else if(global.Promise) { //Promise가 사용가능할때
  4. ...
  5. }

이 함수를 호출할때 bind(), call(), apply() 로 this 를 변경하지 않는다면 this 는 항상 window 또는 global이 될 것입니다.

아무 조건에도 안걸리는 경우엔 잘못 호출한 것입니다. 이에 대한 에러 처리까지 추가하면 아래와 같이 완성됩니다.

  1. function test1(cb) { //파라미터 추가
  2. var global = this;
  3. if(cb) { //콜백함수가 지정되었을때
  4. setTimeout(function() {
  5. cb('test1');
  6. }, 2000);
  7. }
  8. else if(global.Promise) { //Promise가 사용가능할때
  9. return new Promise(function(resolve, reject) {
  10. setTimeout(function() {
  11. resolve('test1');
  12. }, 2000);
  13. });
  14. }
  15. else {
  16. console.error("Callback function isn't define");
  17. }
  18. }

그런데 한가지 거슬리는 부분이 있죠? 처리 로직에 해당하는 부분이 콜백 조건과 프로미스 조건에 각각 존재 합니다. test2 함수는 test1 함수와 같기 때문에, test2 함수는 중복 로직이 없도록 수정하겠습니다. 수정된 최종 소스는 맨 밑에 있습니다.

정리

내가 만약 Back-End와 Front-End 양쪽에서 사용할 공통 스크립트를 만드는 개발자라면, 특정 환경이나 조건에서만 동작하는 함수보다는 다양한 환경을 지원하도록 만드는 것이 좋을것입니다. 하지만 그것이 유지보수를 불편하게 하거나 개발 리스크가 되어서는 안되겠죠.
이번 글의 내용은 상당한 장점이 있으므로, 비동기 함수를 만들어야 할때 반드시 적용해 보시기 바랍니다.

최종샘플

  1. function test1(cb) { //파라미터 추가
  2. var global = this;
  3. if(cb) { //콜백함수가 지정되었을때
  4. setTimeout(function() {
  5. cb('test1');
  6. }, 2000);
  7. }
  8. else if(global.Promise) { //Promise가 사용가능할때
  9. return new Promise(function(resolve, reject) {
  10. setTimeout(function() {
  11. resolve('test1');
  12. }, 2000);
  13. });
  14. }
  15. else {
  16. console.error("Callback function isn't define");
  17. }
  18. }
  19. function test2(cb) {
  20. var global = this;
  21. function _test2(_cb) {
  22. setTimeout(function() {
  23. _cb('test2');
  24. }, 1000);
  25. }
  26. if(cb) {
  27. _test2(cb);
  28. }
  29. else if(global.Promise) {
  30. return new Promise(function(resolve, reject) {
  31. _test2(resolve);
  32. });
  33. }
  34. else {
  35. console.error("Callback function isn't define");
  36. }
  37. }
  38. console.time();
  39. // 콜백함수 구조
  40. test1(function(v1) {
  41. console.log(v1);
  42. test2(function(v2) {
  43. console.log(v2);
  44. console.timeEnd();
  45. })
  46. });
  47. // Promise 구조
  48. // test1().then(function(v1) {
  49. // console.log(v1);
  50. // return test2();
  51. // }).then(function(v2) {
  52. // console.log(v2);
  53. // console.timeEnd();
  54. // });
  55. // async/await 구조
  56. // (async function() {
  57. // let aa = await test1();
  58. // let bb = await test2();
  59. // console.log(aa, bb);
  60. // console.timeEnd();
  61. // })();