Tiny Star

프로젝트/뉴스 요약 (AI)

[뉴스 요약] SpringBoot + React 이메일 인증 기반 회원가입

하얀지 2025. 6. 28. 17:05

전체 흐름

이메일 인증을 포함한 회원가입 기능을 구현했다.
단순한 입력폼과 회원 저장이 아니라, 인증 흐름과 사용자 경험까지 고려한 구조로 만들고자 했다.
이 글에서는 프론트와 백엔드 양쪽에서 회원가입 기능이 어떻게 동작하는지 전체 흐름을 정리해봤다.

 

 

전체 흐름

  1. 사용자가 이메일을 입력하고 인증코드를 요청한다. (`/send-code`)
  2. 인증코드가 메일로 전송되고, 서버는 Redis에 인증코드를 저장한다.
  3. 사용자가 인증코드를 입력해 검증한다. (`/verify-code`)
  4. 인증이 성공하면 Redis에 "인증 완료" 상태를 저장한다.
  5. 닉네임/비밀번호를 입력하고 가입을 요청한다. (`/signup`)
  6. 서버는 Redis 인증 여부를 확인하고, DB에 사용자 정보를 저장한다.

/send-code

/send-code

 

/verify-code

/verify-code

 

 

/signup

/signup

 

 

 

프론트 (Signup.tsx)

1. 이메일 인증 코드 요청

const sendEmailCode = async () => {
  if (isSending || codeVerified || resendTimer > 0) return;
  if (!isEmailValid(email)) {
    alert('이메일은 naver.com 또는 kakao.com 도메인만 사용 가능합니다.');
    return;
  }

  setIsSending(true);
  try {
    await sendEmailCodeApi(email);
    setCodeSent(true);
    setResendTimer(60);
  } catch (err) {
    alert('인증코드 전송 실패');
  } finally {
    setIsSending(false);
  }
};

 

  • 이메일 도메인 제한
  • 재전송 제한 타이머 적용
  • 전송 성공 시 UI 반영

 

2. 인증 코드 입력 및 검증

const verifyCode = async () => {
  try {
    await verifyEmailCodeApi(email, emailCode);
    setCodeVerified(true);
    alert('인증 성공!');
  } catch (err) {
    setVerificationError('코드가 일치하지 않습니다.');
  }
};

 

 

 

 

3. 회원가입 요청

const signup = async () => {
  if (!codeVerified || !passwordValid || !passwordsMatch || !nicknameValid) {
    alert('입력값을 다시 확인해주세요');
    return;
  }

  try {
    await signupApi(email, password, nickname);
    alert('회원가입 완료!');
    navigate('/');
  } catch (err) {
    alert('회원가입 실패');
  }
};

 

 

 

백엔드 (AuthService)

1. 이메일 인증 코드 전송

public void sendEmailCode(String email, HttpServletRequest request) {
    if (!email.endsWith("@naver.com") && !email.endsWith("@kakao.com")) {
        throw new CustomException(ErrorCode.EMAIL_DOMAIN_NOT_ALLOWED);
    }

    if (userRepository.existsByEmail(email)) {
        throw new CustomException(ErrorCode.EMAIL_ALREADY_EXISTS);
    }

    String code = String.format("%06d", new Random().nextInt(1000000));
    redisTemplate.opsForValue().set("email_code:" + email, code, Duration.ofMinutes(5));
    emailService.sendCode(email, code);
}

 

  • 도메인 체크, 중복 이메일 체크
  • Redis 저장 (5분 TTL)
  • 메일 전송

이메일 인증을 요청하면, 먼저 이메일 주소가 허용된 도메인인지 확인한다. 현재는 naver.com과 kakao.com만 허용하고 있고, 가짜 이메일 인증을 방지하기 위해서다.

그다음에는 해당 이메일이 이미 가입되어 있는지도 체크한다. 중복된 이메일에는 인증 코드를 보내지 않도록 해서, 불필요한 리소스 낭비를 줄이고 사용자 경험도 보호할 수 있다.

모든 조건을 통과하면, 6자리 인증 코드를 생성해서 `email_code:{email}` 형태로 Redis에 5분간 저장하고 이후 인증 코드를 메일로 전송하며, 요청 성공 여부는 인증 로그에 함께 저장해두었다.

 

중복 이메일 체크

 

 

2. 인증 코드 검증

public void verifyEmailCode(EmailCodeVerifyRequest verifyRequest, HttpServletRequest request) {
    String key = "email_code:" + verifyRequest.getEmail();
    String savedCode = redisTemplate.opsForValue().get(key);

    if (savedCode == null || !savedCode.equals(verifyRequest.getCode())) {
        throw new CustomException(ErrorCode.EMAIL_CODE_INVALID);
    }

    redisTemplate.delete(key);
    redisTemplate.opsForValue().set("email_verified:" + verifyRequest.getEmail(), "true", Duration.ofMinutes(10));
}

 

 

  • 저장된 코드와 비교
  • 일치하면 인증 상태를 별도 키로 저장

사용자가 입력한 인증 코드가 Redis에 저장된 값과 정확히 일치하는지 확인한다.
일치하지 않거나 코드가 만료된 경우에는 실패 응답을 주고, 성공한 경우에는 `email_verified:{email}` 값을 "true"로 설정하여 Redis에 10분간 저장한다.

이 키는 이후 회원가입 단계에서 이메일 인증이 완료되었는지를 판단하는 기준이 된다.
인증 성공 여부와 함께 인증 로그도 저장해서, 나중에 인증 흐름을 추적하거나 실패 원인을 파악할 때 활용할 수 있다.

이메일 인증

 

 

3. 회원가입 처리

public void signup(SignupRequest signupRequest, HttpServletRequest request) {
    if (!"true".equals(redisTemplate.opsForValue().get("email_verified:" + signupRequest.getEmail()))) {
        throw new CustomException(ErrorCode.EMAIL_NOT_VERIFIED);
    }

    if (userRepository.existsByEmail(signupRequest.getEmail())) {
        throw new CustomException(ErrorCode.EMAIL_ALREADY_EXISTS);
    }

    if (userRepository.existsByNickname(signupRequest.getNickname())) {
        throw new CustomException(ErrorCode.NICKNAME_ALREADY_EXISTS);
    }

    String encodedPw = passwordEncoder.encode(signupRequest.getPassword());
    User user = new User(signupRequest.getEmail(), signupRequest.getNickname(), encodedPw, UserStatus.ACTIVE);
    userRepository.save(user);

    redisTemplate.delete("email_verified:" + signupRequest.getEmail());
}

 

 

  • Redis 인증 확인
  • 이메일 & 닉네임 중복 확인
  • Bcrypt 암호화 후 저장

회원가입 요청이 들어오면, 먼저 Redis에 저장된 `email_verified:{email}` 키가 존재하는지 확인한다. 이 값이 없으면 인증이 완료되지 않은 상태로 판단하고 가입을 차단한다.

그다음에는 DB에서 이메일과 닉네임이 중복되는지 확인한다. 이미 사용 중인 값이 있다면 가입을 막고, 사용자에게 관련 메시지를 응답한다.

모든 검증을 통과하면, 입력된 비밀번호를 Bcrypt로 암호화한 뒤 User 엔티티를 생성해서 저장한다.
가입이 완료된 후에는 `email_verified:{email}` 키를 삭제하여 인증 정보가 재사용되지 않도록 한다.

 

회원가입 완료

 

로그

  • 이메일 인증 흐름을 추적할 수 있도록 로그를 남긴다.
  • 인증 요청 실패(예: 잘못된 도메인, 중복 이메일), 인증 코드 검증 실패 등 다양한 예외 상황을 정확히 기록한다.
  • 실제 사용자가 인증 흐름을 제대로 따라갔는지 검토하거나, 문제가 생겼을 때 디버깅 자료로 활용할 수 있다.
  • 인증 관련 요청에 대한 사용자의 IP, 브라우저 정보(User-Agent) 등도 함께 기록해서 보안 관점에서도 분석이 가능하다.

이메일 인증 과정에서 발생할 수 있는 다양한 상황(예를 들어, 도메인 제한, 코드 불일치, 인증 없이 가입 시도 등)을 기록으로 남기기 위해 추가했다. 인증 성공/실패 이력을 모두 저장함으로써, 나중에 문제가 발생했을 때 원인을 추적하거나 사용자 요청에 대응하기 쉬워지고, 인증 흐름의 품질을 점검하거나 악의적인 시도를 식별하는 데도 도움이 된다. 운영 안정성과 보안 측면 모두를 고려한 선택이다.

 

 

마무리

이메일 인증 기능은 자주 쓰이는 기능이지만, 막상 구현하다 보면 고려할 게 꽤 많다. 인증 흐름을 안전하게 유지하면서도 사용자 경험을 해치지 않도록 하는 균형이 중요하다.
이번 구현에서는 Redis를 활용해 인증 상태를 간단하게 관리하고, 예외 상황도 로그로 잘 남길 수 있도록 구성했다. 앞으로 인증 실패 패턴이나 사용자 흐름을 분석하는 데도 유용하게 쓸 수 있을 것 같다.

 
 
 

 

top