본문 바로가기
카테고리 없음

vue3 에서 회원가입 기능을 만들건데 입력한 아이디 정보가 이미 등록된 아이디인지 체크하는 기능을 구현해줘

by 두잇건강 2025. 8. 19.
반응형

아래는 앞서 제공한 TypeScript 예제를 모두 순수 JavaScript로 변환한 코드입니다. 타입 주석, 제네릭, zod 스키마 등 TS 전용 요소를 제거하고, JS 환경에서 그대로 동작하도록 다듬었습니다.

# 1) 디바운스 유틸 (utils/debounce.js)
```js
// utils/debounce.js
export function debounce(fn, delay = 300) {
  let timer;
  return (...args) => {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}
```

# 2) 중복 검사 API 호출 (api/auth.js)
```js
// api/auth.js
export async function checkUsernameExists(username) {
  const res = await fetch('/api/auth/check-username', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username }),
  });
  if (!res.ok) {
    throw new Error(`checkUsernameExists failed: ${res.status}`);
  }
  const data = await res.json();
  return Boolean(data.exists);
}
```

# 3) 컴포저블: 아이디 중복 검사 로직 (composables/useUsernameCheck.js)
```js
// composables/useUsernameCheck.js
import { ref, watch, computed } from 'vue';
import { debounce } from '@/utils/debounce';
import { checkUsernameExists } from '@/api/auth';

const USERNAME_REGEX = /^[a-z0-9_]{4,16}$/;

export function useUsernameCheck() {
  const username = ref('');
  const isFormatValid = ref(null);   // true | false | null
  const isChecking = ref(false);
  const isDuplicate = ref(null);     // true | false | null
  const error = ref(null);

  const validateFormat = (v) => {
    if (!v) return null;
    return USERNAME_REGEX.test(v);
  };

  const doCheck = async (v) => {
    const valid = validateFormat(v);
    isFormatValid.value = valid;
    isDuplicate.value = null;
    error.value = null;

    if (!v || !valid) return;

    try {
      isChecking.value = true;
      const exists = await checkUsernameExists(v);
      isDuplicate.value = exists;
    } catch (e) {
      error.value = '아이디 중복 확인 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.';
      isDuplicate.value = null;
    } finally {
      isChecking.value = false;
    }
  };

  const debouncedCheck = debounce(doCheck, 400);

  watch(username, (v) => {
    if (!v) {
      isFormatValid.value = null;
      isDuplicate.value = null;
      error.value = null;
      return;
    }
    debouncedCheck(v);
  });

  const checkNow = () => doCheck(username.value);

  const canSubmit = computed(() => {
    return (
      isFormatValid.value === true &&
      isDuplicate.value === false &&
      !isChecking.value &&
      !error.value
    );
  });

  return {
    username,
    isFormatValid,
    isChecking,
    isDuplicate,
    error,
    checkNow,
    canSubmit,
  };
}
```

# 4) 컴포넌트: 입력 UI (components/SignUpUsernameField.vue)
```vue
<!-- components/SignUpUsernameField.vue -->
<script setup>
import { computed } from 'vue';
import { useUsernameCheck } from '@/composables/useUsernameCheck';

const {
  username,
  isFormatValid,
  isChecking,
  isDuplicate,
  error,
  checkNow,
  canSubmit,
} = useUsernameCheck();

const helperText = computed(() => {
  if (!username.value) return '소문자/숫자/언더스코어 4~16자';
  if (isChecking.value) return '아이디 확인 중...';
  if (error.value) return error.value;
  if (isFormatValid.value === false) return '형식이 올바르지 않습니다.';
  if (isDuplicate.value === true) return '이미 사용 중인 아이디입니다.';
  if (isDuplicate.value === false) return '사용 가능한 아이디입니다.';
  return '';
});
</script>

<template>
  <div class="field">
    <label for="username">아이디</label>
    <div class="row">
      <input
        id="username"
        v-model.trim="username"
        type="text"
        placeholder="아이디를 입력하세요"
        autocomplete="username"
        inputmode="latin"
      />
      <button type="button" @click="checkNow" :disabled="!username || isChecking">
        중복 확인
      </button>
    </div>
    <p
      class="helper"
      :class="{
        error: isFormatValid === false || isDuplicate === true || !!error,
        success: isDuplicate === false
      }"
    >
      {{ helperText }}
    </p>

    <button type="submit" :disabled="!canSubmit">회원가입</button>
  </div>
</template>

<style scoped>
.field { display: grid; gap: 8px; }
.row { display: flex; gap: 8px; align-items: center; }
.helper { font-size: 12px; margin: 0; }
.helper.error { color: #d32f2f; }
.helper.success { color: #2e7d32; }
</style>
```

# 5) 서버 예시: Node/Express (server.js)
TypeScript, zod 제거 버전으로 간단한 검증만 JS로 처리합니다.

```js
// server.js
import express from 'express';

const app = express();
app.use(express.json());

// 매우 단순한 형식 검증(프런트와 동일한 정규식)
const USERNAME_REGEX = /^[a-z0-9_]{4,16}$/;

// 예시: 이미 존재하는 아이디 조회 함수(실서비스에서는 DB 사용)
async function isUsernameTaken(username) {
  // TODO: DB 조회로 교체
  const taken = new Set(['admin', 'testuser', 'guest']);
  return taken.has(username);
}

app.post('/api/auth/check-username', async (req, res) => {
  const { username } = req.body || {};

  if (typeof username !== 'string' || !USERNAME_REGEX.test(username)) {
    return res.status(400).json({ message: 'invalid username' });
  }

  try {
    const exists = await isUsernameTaken(username);
    return res.json({ exists });
  } catch (e) {
    return res.status(500).json({ message: 'internal error' });
  }
});

app.listen(3000, () => {
  console.log('Server started on http://localhost:3000');
});
```

# 메모
- import 경로 별칭(@/...)은 Vite/webpack 설정에 따라 다릅니다. 별칭을 쓰지 않는다면 상대경로로 바꾸세요.
- 타입 제거 외에는 로직 변경이 없습니다. ESM이 아닌 CommonJS 환경이라면 import/export 대신 require/module.exports를 사용하세요.
- 테스트 환경(Vitest/Jest)도 타입 정의 없이 그대로 동작하도록 설정 가능합니다. 필요하면 예제 추가해 드리겠습니다.

출처

반응형