About JavaScript 비동기 - (2)

kihunism·2022년 8월 30일
2

JavaScript의 비동기 처리

async, await

하기 전에, 비동기의 반댓말인 동기를 알고 가자.

운영체제(OS)에서 스케쥴링을 공부하다보면, 싱크로나이제이션을 배우는데, 동기화라고 한다. 그럼 코드에서 동기화, 동기란, 순차적인 흐름을 뜻한다. 전 포스팅에서 비동기를 알아봤다. 그래서 동기적인 흐름을 모르는 채 비동기 비동기 하다가 동기적인 흐름을 깨닫지 못 할 수 있으니 짚고 넘어 가자.

다음의 코드를 비교 해보자.

프로미스를 이용한 axios 요청

() => {
	axios.get('url')
	.then((res) => {
    	console.log(res)
        // 및 결과값 (res)를 통한 성공 시 핸들링
    })
    .catch((err) => {
    	console.log(err)
        // 및 결과값 (err)를 통한 실패 시 핸들링(에러 핸들링)
    })
}

async와 await를 이용한 axios 요청

async () => {
	const result = await axios.get('url')

	console.log(result)
    // 및 결과값 result를 통한 핸들링
}

이다. 이 포스팅의 주제는 async, await이므로 두 번째 코드를 중점적으로 봐야 할 것이다!

일단 두 코드의 비교를 본다면

공통점: 필요한url에 axios 요청을 보내, 결과값을 받아와 핸들링 한다.

차이점: 첫 번째 코드는 성공, 실패 핸들링이 분리 되어 있고, 두 번째 코드는 성공, 실패 핸들링이 명시되어 있지 않다.
----> async, await 실패 핸들링은 잠시 후에 다룰 것!

먼저, 그럼 asyncawait가 뭔데? 왜 두 번째 코드에서 대뜸 쓰고 차이점을 알아 봤는지 봅시다.

프로미스 요청의 자세한 내용은 전 포스팅을 참고 하시면 됩니다.
간단하게 첫 번째는 프로미스를 통해서 성공시 then, 실패시 catch의 콜백함수로 진행이 됩니다.

두 번째의 async와 await를 다시 보면, 무언가 동기적으로 진행이 되는 것을 알아 채야 합니다. 알아보기 힘들면 다음의 코드도 한 번 보도록 하자

async (req, res) => {
	const { email, password } = req.body
    const checkEmail = await User.findOne({ where: {email: email} })
    
    if(!checkEmail) {
    	return res.status(401)
        .
        .
    }
}

위 코드는 백엔드에서 로그인을 받았을 때, 실행하는 로직 중에서 async, await를 사용한 부분만 가져왔는데, User.findOne앞에 await가 붙은 것을 알 수 있다. findOne() 이라는 함수가 비동기함수인데, 비동기 함수 앞에 await를 붙여 줌으로써, 동기적인 흐름처럼 코드가 진행 된다. 바로 밑에 if문안에 비동기함수에서 받은 값인 checkEmail을 이용해 분기를 진행하고 있다!

지금 프로그래밍적으로 설명을 했다. 하지만 전 포스팅에 있는 글을 가져와서 다시 보자.

철수야, A상점가서 물건1 사고 그 물건1로 B상점가서 물건2로 바꾸고 받은 물건2로 C상점가서 팔고, 받은 돈으로 D상점가서 쌀사고, 쌀로 집에와서 밥하고, 밥하는 시간에 반찬준비좀 하고 나를 불러라!

이 문장에서 보면, 철수가 A상점에서 물건1을 사는 행위가 끝이나면 그 물건1로 B상점가서 물건2로 바꾸는 행위를 한다.

콜백의 관점에서는 행위 === 함수가 되므로 행위에 행위를 더하는 것! 즉, 함수에 함수를 넣게 되는 것!

프로미스 관점에서는 사용자가 규약을 정하는 것! 행위가 끝나면 then으로 받아서 거기에 또 행위를 넣어서 하는 것! 단 실패하면 catch로 받기, 즉, 철수가 A상점에서 물건1을 사는 것을 성공하면 B상점가서 물건1로 물건2를 바꾸는 것을 then으로 넣고 실패했을 경우에 예를 들어 '집에와라' 라는 행위는 catch에 넣는 것!

그렇다면 await, async는??

프로그래밍적, 의미적으로는 콜백과 프로미스와 동일하다. 그렇다면 다른 점은 철수가 A상점에가서 물건1을 사는 행위를 한다. 그렇다면 결과물은 물건1이 된다. A상점에가서 물건1을 사는 것은 프로그래밍적으로 비동기에 해당한다. 그럼 코드 상에서 'A상점에서 사는 동안 기다려주세요' 해야 한다. 왜 기다려 달라 해야 한다고 물어본다면 그 답은 JavaScript라서 입니다. 그렇다면 그 기다려주세요 라고 하는 것을 코드에서 나타내는게 await이다. 그리고! await는 async 함수 안에서만 사용해야 한다.

async 함수,

일단, 무조건 프로미스 형태를 반환. 프로미스가 아닌 일반 원시자료형을 반환해도 성공된 프로미스를 반환하게 한다.
반환된 프로미스는 then()으로 체이닝이 가능!
위에서 설명한 await가 기다려주세요 인데, async 함수에서만 사용이 가능하다라고 되어 있다. 문법상, 구문상 이해하는데 있어서 문제는 없지만 await를 사용하기 위해서 async를 써야 한다는 것은 잘 못된 것! 하지만 거의 대부분 async, await가 쌍으로 다니기 때문에 크게 문제는 없지만 await를 사용하지 않는 async함수는 그 자체로 반환값이 프로미스가 되므로 이런 코드를 만났을 때에도 문제 없이 읽기위해 정확히 알아 두어야 한다!

조금 이해가 안 된다면 철수 이야기를 예를 들자면, 철수의 어머니가 철수보고 슈퍼가서 간장 사와 한다면 그냥 1가지의 일이기 때문에 비동기적으로 처리 할 필요가 없다. 하지만 슈퍼가서 설탕 사고, 그 설탕으로 찌개에 좀 넣어라 라고 합니다. 그렇다면 2 가지의 일을 해야 되는데, 동시에 할 수가 없다.
이걸 코드상에서 본다면

() => {
	const 설탕 = 설탕구매('설탕구매url')
    찌개에설탕넣기(설탕)
}

위의 코드에서 설탕구매는 비동기함수라 가정합니다. 이렇게 되면 찌개에 설탕을 넣어야 하는데 넣을 수가 없습니다. 왜냐하면 비동기함수가 다 끝나야 설탕을 가지는데 끝이 안났기 때문이죠.
그럼 이걸 프로미스로 본다면

() => {
	설탕구매('설탕구매url')
    	.then((설탕) => {
       		찌개에설탕넣기(설탕) 
        })
        catch((에러) => {
        	console.log(에러)
            // 설탕이 없었음...
        })
}

이렇게 됩니다.

자 그렇다면 앞에 async를 붙이는 이유가 단순히 await를 쓰기위해서가 아닌 await를 썼다면 표시를 해주는 거라고 위에서 했습니다.
철수 어머니가 2가지의 일을 동시에 시켜서 이렇게 말합니다 잘 들어, 까먹지마, 설탕사와서 찌개에 넣어 라고 합니다. 마치 잘 들어, 까먹지마 이렇게 처음에 말하겠죠. 코드 상에서도 async를 붙이면 프로미스를 반환합니다. 라고 선언한 것! 또는 비동기함수가 있으므로 알아보세요 하는 것! 그리고 그걸 동기적으로 처리하는 흐름이 있습니다. 라고 말하는 겁니다. 단순히 'await를 쓰려면 async를 붙여야해'가 아닙니다.

async () => {
	const 설탕 = await 설탕구매('설탕구매url')
    찌개에설탕넣기(설탕)
}

이렇게 설탕구매 할 때까지 기다려주세요. 하고 설탕이 오면 찌개에 넣습니다.

async, await의 에러 핸들링

Promise는 catch를 통해 하는데, async await는 ??????
바로 유명한 try..catch..finally를 이용합니다.

그렇다면 finally는 제외하고 try catch를 이용해서 async와 await도 에러 핸들링하는 코드를 만들어 보면

async () => {
	try{
      	const 설탕 = await 설탕구매('설탕구매url')
      	찌개에설탕넣기(설탕)
    }catch(err) {
    	console.log(err)
    }
}

이렇게 됩니다. 즉 설탕을 구매를 못 하였을 시에는 catch 구문에서 에러를 핸들링하면 된다는 것!

하지만 B U T......

try, catch는 예외처리 구문입니다. try 구문에서 실패와 성공을 어떻게 구분 할까요??
분명히 위의 코드와 설명을 보면 반드시 의문을 제기해야 합니다.
그래서 다음의 백엔드 코드(약식)를 보겠습니다.

async (req, res) => {
	const { email, password } = req.body
    const checkEmail = await User.findOne({ where: {email: email} })
    const checkPassword = await User.findOne({ where: {password: password} })
    
    if(!checkEmail) {
    	return res.status(401)
        .
        .
    }
   	if(!checkPassword) {
    	return res.status(401)
        .
        .
    }
    
    return res.status(200)
    .
    .
}

이렇게 설계가 되어있는 api에서 성공과 실패의 기준은 무엇일까? 그리고 예외의 기준은 어디에 부합하는가? 하는 무조건 의문이 들어야 합니다.

위의 api에서 이메일이나 비밀번호가 일치하지 않으면 401의 응답을 합니다. 그렇게 되면 클라이언트에서 axios요청을 했을 때, 응답이 왔으므로 성공이 아닌가? 합니다. 왜냐하면 응답이 왔기 때문에 요청은 성공이기 때문에 try catch에서 try구문을 벗어나지 않습니다.

그러나, 하지만, B U T 저 api에 프론트에서 비밀번호나 이메일을 틀리게 요청하면 응답이 왔음에도 불구하고 catch구문을 타게 됩니다. 예외처리가 되는 거죠. 처음에는 '아~~ 자바스크립트 try, catch문이 알아서 해주는 구나라고 생각했는데, 그게 아니라 프로토콜에 있었습니다. 그렇다면 역으로 비밀번호가 틀려도 api에서 200대의 응답을 주었을 경우에는 try구문을 벗어나지 않습니다. 마찬가지로 둘 다 일치해도 응답을 200대신에 400대의 응답을 주면 try구문을 벗어나 catch구문으로 가게 됩니다.

프로토콜 -> 규약

약속을 했습니다. 서버기준으로 클라이언트가 실패하면 다 니탓 이므로 400대의 응답번호, 서버가 실패하면 내 탓 500대의 응답번호를 주고 받기로 약속했습니다. 그리고 성공하면 200대의 응답 주기로.... axios로 요청을 한 결과 axios에서 알아서 처리를 해주었습니다. 서버에서 400대의 응답번호를 주었으므로 axios는 예외를 알리게 되고 try구문에서 감지를 해서 catch로 가게 되는 것!

프로미스 axios의 then, catch

성공적인 응답이 오면 then에서 처리를 합니다. 마찬가지로 실패를 할 경우 catch에서 처리를 하게 됩니다. 즉, axios의 프로미스는 성공과 실패의 기준을 응답에 따라 다르게 해놓은 것!

프로미스 지옥 -> async, await

결국 프로미스는 콜백의 문제를 보완하기 위해 나온 것이지만, 프로미스도 프로미스 지옥에 빠질 수 있다. 무한한 then() 체이닝.... 그렇게 되면 코드의 가독성 또한 떨어질 수 밖에 없다. 이 때, async와 await를 이용해서 코드의 줄 수 또한 줄일 수 있으며, 가독성은 마치 동기적인 흐름과 유사하게 되므로 올라가게 된다.

profile
Code Occulter

0개의 댓글