1. 시작하며
Vue나 자바스크립트 좀 만지다 보면 꼭 만나는 게 바로 '비동기 처리'다. 서버에서 데이터를 가져오거나 타이머를 돌릴 때 우리는 코드가 꼬이는 걸 막기 위해 Promise와 .then()을 사용해 왔다.
하지만 로직이 조금만 복잡해져도 코드가 꼬리에 꼬리를 물면서 가독성이 바닥을 친다. 오늘은 내가 직접 코드를 짜며 뼈저리게 느낀 async/await의 강력함과, 우리가 왜 Promise에서 넘어가야 하는지 그 이유를 정리해 보려고 한다.
2. Promise도 순서대로 실행되는데, 왜 굳이 async/await를 쓸까?
사실 .then()도 "이거 끝나면 다음 거 해"라는 뜻이니까 순서대로 작동하는 건 맞다. 그런데도 개발자들이 async/await에 열광하는 가장 큰 이유는 바로 '변수 공유(스코프)'와 '가독성' 때문이다.
예를 들어, "로그인 -> 프로필 조회 -> 알림 설정 조회"라는 3단계 작업을 한다고 가정해 보자. 여기서 핵심은 마지막 3단계에서 1단계(user)와 2단계(profile)의 데이터가 모두 필요하다는 점이다.
[방법 1] 기존 Promise의 고충 (변수 전달의 늪)
function loadUserData() {
login('itkw87')
.then(user => {
console.log('로그인 성공:', user)
// user 데이터를 다음 단계에서도 쓰려면 결국 안으로 기어 들어가야 한다.
return getProfile(user.id).then(profile => {
console.log('프로필 로드:', profile)
// 여기서 1단계의 user와 2단계의 profile을 같이 사용한다.
// 코드가 오른쪽으로 점점 밀려나는(Nesting) 최악의 상황이 발생한다.
return getNotifications(user, profile)
})
})
.catch(err => {
console.error('어디선가 에러 남:', err)
})
}
보이는가? 앞 단계의 데이터를 뒷 단계에서 쓰려고 하니 코드가 V자 형태로 오른쪽으로 파고들기 시작한다. 이걸 흔히 '콜백 지옥' 혹은 'Promise 지옥'이라고 부른다. 에러가 났을 때 추적하기도 굉장히 피곤해진다.
[방법 2] async/await 방식
위의 끔찍한 코드를 async/await로 바꾸면 마법이 일어난다.
async function loadUserData() {
try {
const user = await login('itkw87')
console.log('로그인 성공:', user)
const profile = await getProfile(user.id)
console.log('프로필 로드:', profile)
// 이미 같은 블록(스코프) 안에 변수가 선언되어 있으니 그냥 가져다 쓰면 끝이다!
const notis = await getNotifications(user, profile)
console.log('알림 설정:', notis)
} catch (err) {
console.error('에러 처리도 한방에:', err)
}
}
이게 바로 우리가 async/await를 써야 하는 진짜 이유다. 비동기 코드임에도 불구하고 일반적인 동기 코드처럼 위에서 아래로 물 흐르듯 읽힌다. 변수를 넘겨주기 위해 억지로 코드를 중첩시킬 필요 없이, 위에서 선언한 변수를 아래에서 자유롭게 꺼내 쓸 수 있다. 가독성 차이가 어마어마하다.
3. 핵심 개념 (이것만 알면 끝)
① async (비동기 함수의 시작)
함수 앞에 async를 붙이면, 그 함수는 무조건 Promise를 반환하는 특별한 함수가 된다. "나 이제부터 비동기 처리할 거야!"라고 선언하는 것과 같다.
② await (기다림의 미학)
async가 붙은 함수 안에서만 쓸 수 있다. 비동기 작업(API 호출 등)이 끝날 때까지 함수의 실행을 잠시 멈추고 기다려준다. 데이터를 다 받아오기 전에는 다음 줄로 넘어가지 않기 때문에 안전하게 변수에 값을 담을 수 있다.
4. 실전 적용 사례 (회원가입 로직)
아래는 회원가입 로직이다. axios로 서버와 통신할 때 async/await를 쓰면 코드가 깔끔하다.
const handleJoin = async () => {
try {
// 서버에 가입 요청을 보내고 응답이 올 때까지 여기서 딱 대기한다.
const response = await axios.post('/api/user/join', {
emil: email.value,
nkNm: nickname.value,
pswd: password.value
})
// 응답이 무사히 오면 성공 처리
if (response.status === 201) {
alert("가입 성공! 환영합니다.")
}
} catch (error) {
// 서버에서 에러를 던지면 복잡하게 .catch() 찾을 필요 없이 바로 여기로 튀어온다.
const msg = error.response?.data?.message || "오류가 발생했습니다."
alert(msg)
}
}
5. 주의할 점
- 에러 처리는 무조건 try-catch: await를 쓸 때 try-catch로 감싸주지 않으면 통신 실패 시 브라우저가 그냥 뻗어버릴 수 있다. 에러 핸들링은 선택이 아닌 필수다.
- 함수 앞에 async 빼먹지 말기: 은근히 자주 하는 실수다. 내부에 await를 쓰려면 반드시 그 코드를 감싸고 있는 함수에 async 키워드가 있어야 한다.
6.마치며
프로그래밍 코드는 결국 컴퓨터가 아니라 사람이 읽는 거다. 나중에 내가 짠 코드를 다시 봤을 때 "이게 뭔 소리야?" 하며 과거의 나를 원망하지 않으려면 async/await는 무조건 써야 한다.
'[Language] JavaScript' 카테고리의 다른 글
| [Language/JavaScript] Truthy (참으로 평가되는 값) 와 Falsy (거짓으로 평가되는 값) (17) | 2024.02.12 |
|---|---|
| [Language/JavaScript] 자바 스크립트(Java Script) 함수 선언 방법 3가지 (71) | 2024.01.08 |