회원가입 과정에서 닉네임 중복 여부를 입력 즉시 안내하고 싶었다.
하지만 타이핑할 때마다 서버에 요청을 보내는 구조는 불필요한 API 호출 증가로 이어질 수 있다.
그래서 다음 원칙을 세웠다.
닉네임 입력 필드는 input 이벤트를 사용하고 있었고,
아무런 제어 없이 구현하면 한 글자 입력할 때마다 중복 검사 API가 호출된다.
t → te → tes → test
이 과정에서 서버는 같은 목적의 요청을 여러 번 처리하게 되고, 불필요한 API 호출이 반복적으로 발생할 수 있다.
💡 아래에는 설명에 필요한 로직만 발췌해 정리했다.
가장 먼저 적용한 것은 디바운싱이다.
사용자가 타이핑을 멈춘 뒤 일정 시간(300ms)이 지나면 그때만 서버 요청을 보낸다.
let nicknameCheckTimer = null;
nicknameInput.addEventListener("input", () => {
const nickname = nicknameInput.value.trim();
clearTimeout(nicknameCheckTimer);
nicknameCheckTimer = setTimeout(() => {
// 닉네임 중복 검사 호출
}, 300);
});
이것만으로도 연속 입력으로 발생하는 요청 대부분을 줄일 수 있다.
하지만 여전히 문제가 하나 남아 있었다.
사용자가 이런 식으로 입력하는 경우를 생각해보자.
test → test1 → test
디바운싱을 적용해도, 이미 검사했던 "test"에 대해 다시 서버 요청이 발생한다.
그래서 이전 검사 결과를 메모리 캐시(Map)에 저장했다.
const nicknameCache = new Map(); // nickname -> duplicate 여부
async function checkNicknameDuplicate(nickname) {
// 1) 캐시가 있으면 서버 요청 없이 반환
if (nicknameCache.has(nickname)) {
return nicknameCache.get(nickname);
}
// 2) 서버 요청
const response = await fetch(`/auth/check-nickname?nickname=${encodeURIComponent(nickname)}`);
const { duplicate } = await response.json();
// 3) 결과 캐싱
nicknameCache.set(nickname, duplicate);
return duplicate;
}
이렇게 하면 같은 닉네임을 다시 입력했을 때 API 호출을 생략할 수 있다.
캐시는 편리하지만, 항상 최신 상태를 보장하지는 않는다.
1. A 사용자가 "test123" 검사 → 사용 가능 (캐싱)
2. B 사용자가 같은 닉네임으로 회원가입 완료
3. A 사용자가 다시 "test123" 입력 → 캐시 사용 → 사용 가능 표시 ⚠️
이를 방지하기 위해 캐시에 유효기간(TTL)을 두었다.
const nicknameCache = new Map(); // nickname -> { duplicate, timestamp }
const CACHE_EXPIRY_TIME = 30_000; // 30초
async function checkNicknameDuplicate(nickname, forceRefresh = false) {
// 캐시 확인 (강제 재검증이 아닐 때만)
if (!forceRefresh && nicknameCache.has(nickname)) {
const cached = nicknameCache.get(nickname);
// TTL 유효하면 캐시 반환
if (Date.now() - cached.timestamp < CACHE_EXPIRY_TIME) {
return cached.duplicate;
}
// TTL 만료 → 명시적으로 캐시 무효화
nicknameCache.delete(nickname);
}
// 서버 요청
try {
const res = await fetch(`/auth/check-nickname?nickname=${encodeURIComponent(nickname)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { duplicate } = await res.json();
nicknameCache.set(nickname, {
duplicate,
timestamp: Date.now(),
});
return duplicate;
} catch {
return null; // 검사 실패
}
}
입력 중 검사는 UX를 위한 참고 정보일 뿐이고, 데이터가 확정되는 순간은 폼 제출 시점이다.
그래서 최종 제출 시에는 캐시를 무시하고 서버를 다시 확인한다.
form.addEventListener("submit", async (e) => {
e.preventDefault();
const nickname = nicknameInput.value.trim();
const isDuplicate = await checkNicknameDuplicate(nickname, true); // 강제 재검증
if (isDuplicate === null) {
alert("닉네임 확인 중 오류가 발생했습니다.");
return;
}
if (isDuplicate) {
showError("이미 사용 중인 닉네임입니다.");
return;
}
// 여기서부터 실제 회원가입 로직 (생략)
});
fetch는 네트워크 오류에만 reject되고, 4xx/5xx 응답은 정상적으로 resolve된다.
그래서 res.ok로 HTTP 상태를 명시적으로 확인하고,
예외 발생 시 catch 블록에서 null을 반환하도록 했다.
try {
const res = await fetch(...);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
// ...
} catch {
return null; // 검사 실패
}
이렇게 하면 사용 가능(false) / 중복(true) / 확인 불가(null) 세 가지 상태를 구분할 수 있다.
만약 에러를 false로 처리하면, 네트워크 장애 상황에서도 중복이 아닌 것으로 판단되어 잘못된 안내가 발생할 수 있다.
입력이 빠르게 변경되는 상황에서는 여러 중복 검사 요청이 동시에 진행될 수 있다.
이때 이전 요청이 늦게 도착하면, 최신 입력 상태를 과거 결과로 덮는 race condition 문제가 발생할 수 있다.
예를 들어 다음과 같은 방식으로 보완하는 것도 가능하다.
let currentRequestId = 0;
// input 이벤트 리스너 내부에서
nicknameCheckTimer = setTimeout(async () => {
const requestId = ++currentRequestId;
const isDuplicate = await checkNicknameDuplicate(nickname);
if (requestId !== currentRequestId) return; // 오래된 응답 무시
// UI 업데이트
}, 300);
let abortController = null;
// input 이벤트 리스너 내부에서
nicknameCheckTimer = setTimeout(async () => {
abortController?.abort();
abortController = new AbortController();
const isDuplicate = await checkNicknameDuplicate(
nickname,
false,
abortController.signal
);
if (isDuplicate === null) return;
// UI 업데이트
}, 300);
닉네임 중복 검사는 단순해 보이지만, 입력 빈도가 높고 서버 확인이 필요한 기능이라 생각보다 고려할 부분이 많았다.
디바운싱을 적용해 입력 중 발생하는 불필요한 API 호출을 줄일 수 있었고, 캐싱을 추가하면서 같은 값을 다시 입력하는 경우에는 서버 요청을 생략할 수 있었다.
다만 캐시를 사용하면 결과가 최신 상태와 어긋날 수 있어 유효기간을 두고 최종 제출 시에는 서버 기준으로 다시 확인하도록 했다.
또한 서버 응답을 신뢰할 수 없는 상황에서는 중복 여부를 곧바로 확정하지 않도록 하여 잘못된 안내가 발생할 가능성을 줄일 수 있었다.
입력이 빠르게 변경되는 상황에서 발생할 수 있는 race condition 문제도 살펴보면서, 입력 기반 API 호출에서는 응답 순서나 예외 상황까지 고려해야 한다는 점을 정리할 수 있었다.