Promise 제대로 사용하는 법 — 동기값 반환 때문에 발생하는 .then 오류와 해결
들어가며
JavaScript에서 비동기 작업을 다룰 때 Promise를 일관되게 반환하는 것이 중요합니다.
함수가 Promise 대신 동기값(또는 undefined)을 반환하면 호출부에서 .then/.catch를 호출할 수 없어 런타임 오류가 납니다.
문제 사례 (실제 오류)
에러: Uncaught TypeError: User.isDuplicateLogin(...).then is not a function
원인: isDuplicateLogin이 Promise를 반환하지 않고 boolean 또는 undefined를 반환했기 때문.
나쁜 예 (동기 boolean 반환 — 오류 발생)
// 잘못된 패턴: 동기값을 반환하면 호출부에서 .then을 쓸 수 없음
function isDuplicateLogin(userId) {
// 서버 호출을 했지만 결과를 반환(return)하지 않음
axiosApi.get(`/api/user/${userId}/auth`).then((res) => {
return !!res?.results?.hasActiveSessions; // 이 값은 then 핸들러 내부에서만 사용되고 함수는 undefined 반환
});
}
올바른 예: Promise를 반환하도록 고치기
- 핵심: 내부에서의 비동기 체인을 반드시 return 하여 호출자에게 Promise를 돌려준다.
// 권장: 항상 Promise<boolean>을 반환
function isDuplicateLogin(userId) {
return axiosApi
.get(`/api/user/${encodeURIComponent(userId)}/auth`)
.then((res) => {
if (!res || typeof res.results?.hasActiveSessions === 'undefined') {
// 서버 응답이 기대치와 다르면 에러를 던져 호출자가 판단하게 함
const e = new Error('NO_DUPLICATE_INFO');
e.code = 'NO_DUPLICATE_INFO';
throw e;
}
// boolean으로 표준화해서 반환
return !!res.results.hasActiveSessions;
});
}
호출부 예시 1 — Promise 체인 (.then/.catch/.finally)
// 로그인 성공 후 중복확인 흐름 (async/await 없이)
User.isDuplicateLogin(userId)
.then(function (isDup) {
if (isDup) {
const proceed = confirm('다른 세션이 있습니다. 계속하시겠습니까?');
if (!proceed) throw { handled: true }; // 체인 중단 신호
}
// 중복 검사를 통과한 이후 처리
// 상태 설정, 로컬 정리, 리다이렉트 등
})
.catch(function (err) {
if (err && err.handled) return;
if (err?.code === 'NO_DUPLICATE_INFO') {
alert('서버에서 중복 여부를 확인할 수 없습니다.');
return;
}
// 실제 오류 처리
console.error(err);
alert('중복 확인 중 오류가 발생했습니다.');
})
.finally(function () {
// 로딩 숨김/플래그 복구는 항상 finally에서
utilHideLoadingIndicator();
User.isLoginPending = false;
});
호출부 예시 2 — async/await 스타일
async function handleLoginFlow(userId) {
try {
const isDup = await User.isDuplicateLogin(userId);
if (isDup) {
const proceed = confirm('다른 세션이 있습니다. 계속하시겠습니까?');
if (!proceed) return;
}
// 이후 처리
} catch (err) {
if (err?.code === 'NO_DUPLICATE_INFO') {
alert('서버에서 중복 여부를 확인할 수 없습니다.');
return;
}
if (err?.name === 'CanceledError' || err?.code === 'ERR_CANCELED') return;
console.error(err);
alert('중복 확인 중 오류가 발생했습니다.');
} finally {
utilHideLoadingIndicator();
User.isLoginPending = false;
}
}
이번에 배운점!
항상 Promise를 반환(return)하라. (axios/Fetch는 Promise, jQuery의 $.ajax는 jqXHR(then 사용 가능)임을 유의)
함수는 결과를 boolean/도메인 값으로 표준화해 반환하거나, 오류는 던져(callee에서 throw) 호출자가 판단하도록 하라.
에러를 덮어쓰지 말고 필요한 경우 코드/메시지를 붙여 throw하라(구분용).
상태 복구(utilHideLoadingIndicator, isLoginPending 등)는 finally에서 처리해 레이스 컨디션을 막아라.
취소(AbortController)와 같은 케이스는 호출부와 인터셉터에서 일관되게 무시하도록 처리하라.
결론
Promise를 제대로 반환하고 에러 전파/정규화를 명확히 하면 .then is not a function 같은 오류를 예방할 수 있다.
호출부는 반환된 Promise를 기다리거나 catch에서 적절한 처리를 하면 안전한 비동기 흐름을 만들 수 있다.