Javascript 비동기 프로그래밍의 4가지 방법

Asynchronous code

다음과 같이 유저정보를 가져오는 함수가 있다.

1
2
3
4
5
const getUser = () => {
return {name:'timpac', auth:'admin'}
}

console.log(getUser()); // {name:'timpac', auth:'admin'}

실행하면 유저의 정보가 잘 출력된다.
하지만 getUser() 함수를 실행하는 시간이 좀 걸린다고 가정했을 때도 제대로 값을 가져올까?

1
2
3
4
5
6
7
const getUser = () => {
setTimeout(() => {
return {name:'timpac', auth:'admin'}
}, 2000);
}

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

setTimeout을 이용해 2초 딜레이 시켰더니 undefined가 출력된다.
javascript는 비동기적으로 수행되기 때문에 getUser() 함수가 실행이 끝나기도 전에 console 출력을 하기 때문이다.
순서대로 실행시키기 위해서는 동기화 시켜줘야한다. 동기화 시키는 여러가지 방법에 대해 알아보자

synchronous code

  1. Callback Function
1
2
3
4
5
6
7
const getUser = callback => {
setTimeout(() => {
callback({name:'timpac', auth:'admin'});
}, 2000);
}

getUser(console.log); // {name:'timpac', auth:'admin'}

첫번째는 callback을 이용한 방법이다. getUser()는 함수가 수행된 후에 호출할 Callback function을 인자로 받는다.
리턴값을 콜백함수 인자로 넣어서 호출하면 콜백함수에서 해당값을 사용할 수 있다.
이제 몇가지 작업을 더 추가해보자. 유저정보를 가져와서 권한을 체크하고 싶다.

1
2
3
4
5
const hasAuth = (auth, then) => {
setTimeout(() => {
then(auth === 'admin');
}, 1000);
}

then이라는 이름의 콜백함수를 인자로 넣고 시간은 1초 지연시켰다.
실행코드는 다음과 같다.

1
2
3
4
5
getUser(user => { 
hasAuth(user.auth, result => {
console.log(result);
});
});

이제 관리자 권한이 있다면 유저의 이름을 가져오는 기능을 추가해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const getUserName = (userName, then) => {
setTimeout(() => {
then(userName);
}, 1000);
}

//실행코드
getUser(user => {
hasAuth(user.auth, result => {
console.log(result);
if(result) {
getUserName(user.name, userName => {
console.log(userName);
});
}
});
});

3개의 function을 동기화 시켰을뿐인데 코드읽기가 벌써 힘들어보인다.
아직은 읽을만 하지만 만약 기능들이 계속해서 추가된다면 더욱 읽기가 어려워질 것이다.
위 실행코드처럼 콜백패턴이 계속 이어지는걸 Callback Hell이라고 한다

callback_hell콜백 지옥

  1. Promise
    프로미스를 사용하면 좀더 읽기 편한 코드로 만들 수 있다.
    위 3개의 함수를 프로미스로 바꾸면 아래와 같다
1
2
3
4
5
6
7
8
9
10
11
const getUser = () => new Promise((resolve, reject) => {
setTimeout(() => resolve({name:'timpac', auth:'admin'}), 2000);
});

const hasAuth = auth => new Promise((resolve, reject) => {
setTimeout(() => resolve(auth === 'admin'), 1000);
});

const getUserName = userName => new Promise((resolve, reject) => {
setTimeout(() => resolve(userName), 1000);
});

Promise에 대한 자세한 설명은 생략한다(귀찮..) 링크로 대체 ^^
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise

omit

이제 실행코드를 작성해보자

1
2
3
4
5
6
7
8
9
10
getUser().then(user => {
hasAuth(user.auth).then(result => {
console.log(result);
if(result) {
getUserName(user.name).then(userName => {
console.log(userName);
});
}
});
});

오잉? 이것은 어디서 많이 본듯한 형태인데? 류가 장풍을 쏠것만 같다
callback hell을 벗어나기 위해서 Promise를 썼는데 Promise Hell에 빠져버렸다 ^^

위 코드는 Promise를 제대로 사용하지 못했기 때문에 발생한다.
지옥에서 나오기 위해 Promise chaining을 이용하여 다음과 같이 수정했다.

1
2
3
4
5
6
7
8
getUser().then(user => {
return hasAuth(user.auth)
}).then(result => {
console.log(result);
if(result) {
return getUserName(user.name); // user 접근이 안됨
}
}).then(console.log);

실행하니 user.name 이 undefined로 출력된다. 두번째 then()으로 권한 체크 값만 들어가기 때문에
유저 정보를 참조할 수 없는 문제가 발생한다.
상위chain의 데이터를 참조 하고 싶을땐 Chain을 분리하는 방법, 전역변수를 이용하는 방법 등이 있지만
제일 좋은 방법은 async/await을 이용하는 것이다.

  1. async / await
1
2
3
4
5
6
7
8
9
10
11
const asyncProcess = async () => {
const user = await getUser();
const isAdmin = await hasAuth(user.auth);
console.log(isAdmin);
if(isAdmin) {
const userName = await getUserName(user.name);
console.log(userName);
}
}

asyncProcess();

제대로 동작한다. 코드의 가독성도 좋아졌다

async/await 사용방법

  1. reduce를 이용한 방법
1
2
3
4
5
6
7
8
9
const composeAsync = (...funcs) => x => funcs.reduce((acc,val) => acc.then(val), Promise.resolve(x));

composeAsync(
getUser,
user => user.auth,
hasAuth,
getUserName, //user를 받지 못함
console.log
)();

역시나 상위 함수의 데이터를 가져오지 못하는 문제가 있다. 하나의 데이터를 사용할 때만 써야될 것같다.
방법이 있을 것 같기는한데 잘 모르겠다. 혹시나 아시는 분 있으면 알려주시면 감사~