개발자꿈나무

FureverHomes 프로젝트 - 회원 가입 1. 회원 저장과 중복 확인 본문

기술블로그

FureverHomes 프로젝트 - 회원 가입 1. 회원 저장과 중복 확인

망재이 2023. 9. 19. 23:51

오늘 하루종일 뭘 했는지 잘 모르겠다.. ㅎ 삽질만 하루종일 하다가 겨우 저녁쯤 얼추 개념이 잡혀서 회원을 등록하고, 중복여부를 확인해서 테스트 코드를 짜는 데까지 완료했다. 제일 어려웠던건 아무래도 Spring DATA JPA를 적용하는 것이었다. 여러가지 레퍼런스들을 찾아보다 보니 오히려 더 머릿속이 복잡해지고 가닥도 제대로 잡히지 않고 우당탕탕 시간만 보냈던 것 같다. 

 

 

 

MemberDTO 생성

 

 

Restful API를 사용하여 데이터를 전송할 때 일반적으로 Request와 Response용 DTO를 따로 만드는 것을 권장한다. 

1. MemberRequest: 주로 POST 또는 PUT 요청에서 클라이언트가 서버로 데이터를 전송할 때 사용된다.

2. MemberResponse : 서버에서 클라이언트로 응답할 때 사용되며, 주로 서버의 응답에 사용된다.

 

  • MemberRequestDto
//회원가입 정보가 넘어오는 dto
@Getter
@Setter //테스트용
@NoArgsConstructor
public class MemberReqDto {
    private String email; //이메일
    private String name; //이름
    private String password; //비밀번호
    private Sex sex; //성별
    private LocalDate birth; //생년월일
    private boolean emailAuth; //이메일 인증여부

    public void encodingPassword(PasswordEncoder passwordEncoder) {
        if(ObjectUtils.isEmpty(password)) return;
        password = passwordEncoder.encode(password);
    }

    public Member toEntity() {
        return Member.builder()
                .email(email)
                .name(name)
                .password(password)
                .sex(sex)
                .birth(birth)
                .email_auth(false)
                .build();
    }
}

 

  • MemberResponseDto
@Getter
public class MemberResDto {

    private Long id; //pk
    private String email; //회원 아이디
    private String password; //비밀번호
    private String name; //이름
    private Sex sex; //성별
    private LocalDate birth; //생년월일
    private LocalDate regi_date; //가입날짜
    private Boolean email_auth; //이메일 인증여부
    private List<Animal> animals; //관심동물

    public void clearPassword() {
        this.password = "";
    }
}

 

requestDto에는 비밀번호는 암호화를 위해 spring security에서 제공하는 PasswordEncoder를 사용해서 암호화해줬으며,

responseDto에는 노출되면 안되는 정보이므로 ""로 초기화를 해줬다.

 

 

 

 

MemberRepository

 

 

Spring Data JPA에서는 엔티티 매니저를 직접 이용해 코드를 작성하지 않아도 되는데, 그 역할을 대신 해줄 Repository 인터페이스를 설계해야한다. MemberRepository 인터페이스를 하나 생성해서 JpaRepository를 상속받아야 한다. 첫 번째 제네릭 타입은 엔티티 타입 클래스, 두 번째는 기본키 타입을 넣어준다. save, delete, count, findAll의 기본적인 메소드는 JpaRepository가 지원을 해주며, 나머지 메소드들은 직접 설정해주면 된다.

이때부터 나의 삽질이 시작되었다. 회원 가입 시 꼭 필요한 기본적인 메소드들을 하나하나 적어놓은 상태이다.

1. 회원 정보 저장 : 기본 save() 메소드 사용할 것이다.

2. 회원 가입시 중복 이메일 확인 : 현재 두개의 메소드를 적어놨는데, 이유를 나중에 설명하도록 하겠다. 큰 착각을 했던 부분이다.

3. 이메일 인증 여부 확인 : 이메일 인증 코드를 통해서 가입을 진행할 것이므로, 인증을 제대로 진행하지 않고 프로그램을 종료한 사람들을 확인하기 위한 메소드이다.

4. 회원 정보 삭제 : email을 통해 정보를 삭제할 것이므로 기본 delete() 말고 직접 설정을 해주었다.

//JpaRepository<엔티티 타입 클래스, 기본키 타입>
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    //1. 회원 정보 저장
    //save();

    //2. 중복 이메일 확인 (회원 수 카운팅)
    long countByEmail(String email);

    //3. 이메일 인증 여부 확인
    Member findByEmail(String email); 
    boolean existsByEmail(String email); //잘못 생각한 점을 정리하기 위해 적어둠

    //4. 회원 정보 삭제
    void deleteByEmail(String email);
}

 

 

 

 

MemberService - 수정 전

 

 

<수정 전>

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;

    //회원 저장 (회원가입)
    @Transactional
    public Long saveMember(final MemberReqDto memberDto) {

        //중복회원인 경우
        if (isDuplicateMember(memberDto.getEmail())) {
            return null;
        }

        //중복회원이 아닌 경우
        System.out.println("신규회원입니다.");
        memberDto.encodingPassword(passwordEncoder);
        Member member = memberRepository.save(memberDto.toEntity());
        return member.getId();
    }

    //중복여부 확인
    @Transactional
    public boolean isDuplicateMember(final String email) {
        long existEmail = memberRepository.countByEmail(email);
        if (existEmail != 0) {
            return checkEmailAuth(email);
        }else return false;
    }

    //이메일 인증 여부 확인
    @Transactional
    public boolean checkEmailAuth(final String email) {
        boolean checkEmailAuth = memberRepository.existsByEmail(email);
        if (checkEmailAuth) {
            System.out.println("이미 가입된 회원입니다.");
            return true;
        } else {
            System.out.println("이메일 인증을 받지 않은 회원입니다. 회원 정보를 삭제합니다.");
            //회원정보삭제
            memberRepository.deleteByEmail(email);
            return false;
        }
    }
}

회원 가입을 진행할 때 회원 정보를 입력하고 제출을 하면 이메일 인증 페이지로 이동하게 된다. 인증 페이지에서 이메일 인증을 완료하고, 정상적으로 인증이 된 회원은 인증 정보를 true로 변경할 것이다. 아직 인증을 성공한 경우까지는 구현하지 못했고, 그 전까지 구현을 해놓은 상태인데 입력한 email과 일치하는 회원이 있는지 isDuplicateMember()를 통해서 확인하고 존재하게 되면 checkEmailAuth()를 통해 이메일 인증을 받았는지 확인한다. 인증을 받았으면 이미 가입된 회원이고, 그렇지 않다면 이전에 입력된 데이터를 삭제하고 회원가입을 새로 진행한다.

테스트 코드를 진행하면서 문제점을 발견하게 됐는데 이후에 수정 후 코드를 통해 설명하겠다.

 

 

 

 

MemberRepositoryTest

 

 

  • 회원 가입 테스트 - 신규 회원
@SpringBootTest
class MemberServiceTest {

    @Autowired
    MemberService memberService; //의존성 주입

    @Autowired
    PasswordEncoder passwordEncoder;

    public MemberReqDto createMemberTest1() {
        MemberReqDto memberReqDto = new MemberReqDto();
        memberReqDto.setEmail("test1@test.com");
        memberReqDto.setName("test1");
        memberReqDto.setBirth(LocalDate.now());
        memberReqDto.setPassword("test123");
        memberReqDto.setSex(Sex.M);
        return memberReqDto;
    }
    
    @Test
    @DisplayName("회원가입 테스트 - 신규회원")
    public void saveNewMemberTest() {
        Long memberId = memberService.saveMember(createMemberTest1());
        assertNotNull(memberId);
    }
}

임의의 MemberReqDto 값을 세팅해주고 saveMember()를 진행했을 때 Id값이 제대로 반환되는지를 테스트했다.

 

  • 회원 가입 테스트 - 이메일 인증을 제대로 완료하지 않았던 회원
@SpringBootTest
class MemberServiceTest {

    @Autowired
    MemberService memberService; //의존성 주입

    @Autowired
    PasswordEncoder passwordEncoder;

    public MemberReqDto createMemberTest1() {
        MemberReqDto memberReqDto = new MemberReqDto();
        memberReqDto.setEmail("test1@test.com");
        memberReqDto.setName("test1");
        memberReqDto.setBirth(LocalDate.now());
        memberReqDto.setPassword("test123");
        memberReqDto.setSex(Sex.M);
        return memberReqDto;
    }

    @Test
    @DisplayName("회원가입 테스트 - 이메일 인증 받지 않은 회원")
    public void saveNotAuthTest() {
        MemberReqDto member1 = createMemberTest1();
        //이메일 인증이 안된 회원 정보 저장
        Long memberId1 = memberService.saveMember(member1);
        assertNotNull(memberId1);

        //중복 이메일로 다시 회원가입 시 (이전 정보를 지우고 다시 회원정보 저장)
        boolean isDuplicate = memberService.isDuplicateMember(member1.getEmail());
        if (isDuplicate) {
            //1. 중복되는 이메일이 있을 때 이메일 인증 여부를 확인 - false여야만 함
            boolean isEmailAuth = memberService.checkEmailAuth(member1.getEmail());
            assertFalse(isEmailAuth);
        }
        //2. 새로운 회원 정보 저장
        Long newMember = memberService.saveMember(member1);
        assertNotNull(newMember);
    }

}

여기서 나의 생각의 오류를 제대로 찾을 수 있었는데 예상했던 테스트 결과는 이메일 인증여부가 false이기 때문에 다시 회원가입을 시도할 때 이전 회원 정보를 지우고 isEmailAuth가 false가 나오는 것이었다. 그러나 isEmailAuth가 계속 true가 나왔다. 처음에는 트랜젝션 때문에 롤백이 돼서 true가 반환되는 건 아닐까 생각했었다. 그러나 모든 트랜젝션 어노테이션을 제거해도 여전히 true가 나왔고, repository에서 뭔가 잘못된 부분이 있는건 아닐까 생각했다. 이때 repository에 들어있던 메소드는

boolean existsByEmail(String email);

였다. 이 메소드를 유심히 보다보니 유레카처럼 뭔가가 머릿속을 스쳤다. 쿼리 메소드에 대해서 완전히 잘못 생각하고 있었던 것이다. 

find + (엔티티 이름) + By + 변수이름

이게 기본적인 문법이며 말그대로 (변수이름)에 의해 찾고자하는 (엔티티 이름 혹은 정보)를 나타낸다. 매개변수에 들어가는 값을 By 이후에 오는 변수이름에 들어갈 값이라고 생각하면 된다. 따라서 변수 이름은 꼭 엔티티 필드명과 일치해야 한다. 따라서 내가 적어놓은 메소드를 해석해보면 '해당 Email이 존재하는지 안하는지 결과를 나타내라'인 것이다. 당연히 일치하는 이메일은 존재할 것이고, 일치하는 이메일이 존재할 때 이메일 인증을 받았는지 안 받았는지를 알아내야하는건데 쿼리 메소드에 대해서 제대로 이해를 하지 못한 채로 일단 코드를 짜다보니 이런 어처구니 없는 실수를 했던 것이다.. queryDSL을 통해서 동적 쿼리를 생성해줄까도 생각했지만 그냥 이런 방법을 택했다.

 

 

 

 

MemberService - 수정 후

 

 

 

<수정 후>

//이메일 인증 여부 확인
@Transactional
public boolean checkEmailAuth(final String email) {
    Member member = memberRepository.findByEmail(email);
    if (member != null && member.getEmail_auth()) {
        System.out.println("이미 가입된 회원입니다.");
        return true;
    } else {
        System.out.println("이메일 인증을 받지 않은 회원입니다. 회원 정보를 삭제합니다.");
        //회원정보삭제
        memberRepository.deleteByEmail(email);
        return false;
    }
}

이렇게 수정해준 후에 테스트 코드를 돌렸을 때 정상적으로 돌아가는 것을 확인할 수 있었다.

 

 

 

 

정리

 

 

오늘은 Spring Data JPA와 친해지는 날이었던 것 같다..ㅎㅎ 머릿속이 뒤죽박죽 난리였지만 수많은 시행착오를 겪고 데이터베이스에 값이 딱 들어가는걸 보는 순간 고생했던 게 다 날아가는 기분이였다. 처음이 어렵지 좀 익숙해지면 나머지 기능들은 좀 더 빨리 해낼 수 있지 않을까 싶다. 방금 글을 정리하면서 생각난건데 Service 코드들을 좀 수정할 필요가 있을 것 같다. findByEmail 하나만으로도 회원 가입을 진행할 수 있지 않을까 싶은 생각이 든다. 내일 좀 더 신중히 고민해보고 고쳐봐야할 것 같다.

 

 

 

======================================== 수정 =======================================

다음날이 되니까 전날 썼던 포스팅이 너무 부끄럽고 정리가 하나도 안된 상태로 급하게 쓴 글이라서 비공개 처리를 했었는데, 이런 고군분투했던 과정들도 내가 걸어온 과정들이니 그냥 공개를 해놓기로 했다. 어제 마지막에 블로그 정리를 하고 났더니 Service 코드를 수정할 필요가 있을 것 같다는 생각이 들어서 회원 정보를 저장하고, 중복 회원을 찾는 로직을 다시 정리해봤다.

 

  • MemberService 수정
//회원 저장 (회원가입)
@Transactional
public Long saveMember(final MemberReqDto memberDto) {

    //중복회원인 경우
    if (isDuplicateMember(memberDto.getEmail())) {
        return null;
    }

    //중복회원이 아닌 경우
    System.out.println("신규회원입니다.");
    memberDto.encodingPassword(passwordEncoder);
    Member member = memberRepository.save(memberDto.toEntity());
    return member.getId();
}

//중복여부 확인
@Transactional
public boolean isDuplicateMember(final String email) {
    Member existingMember = memberRepository.findByEmail(email);
    if (existingMember != null && existingMember.getEmail_auth() == 1) { //회원 존재 & 인증도 처리
        return true;
    } else if (existingMember != null && existingMember.getEmail_auth() == 0) { //회원 존재 & 인증 처리 안함
        System.out.println("이메일 인증을 받지 않은 회원입니다. 회원 정보를 삭제합니다.");
        //회원정보삭제
        deleteMember(email);
    } return false; //회원 정보 없음
}

@Transactional
public void deleteMember(final String email) {
    memberRepository.deleteByEmail(email);
}

일치하는 회원의 수가 존재하는지 존재하지 않는지를 확인했던 메소드를 없애버리고 isDuplicateMember() 메소드 안에서 existingMember 객체만을 가지고 한번에 처리를 하도록 수정했다. 가입 정보가 없거나 인증을 받지 않았다면 결론적으로 false를 반환하고, 인증까지 완료한 정보가 있을 경우만 true를 반환하도록 수정했다. 확실히 불필요한 코드가 없어지고, 코드 길이도 짧게 구현할 수 있었다. 그리고 이메일 인증을 하는 방법 때문에 Member 엔티티의 email_Auth를 Boolean이 아니라 int 값으로 변경했다.

 

 

 

 

MemberController

 

 

RestController와 Controller를 함께 적절히 섞어서 사용할 예정이다. 

@Controller
@RequiredArgsConstructor
@RequestMapping("/fureverhomes")
public class MemberController {

    private final MemberService memberService;

    //로그인 회원가입을 위한 메인 페이지
    @GetMapping
    public String getMain() {
        return "html/furever_main";
    }

    //회원가입 페이지
    @GetMapping("/signup")
    public String getSignUp() {
        return "html/furever_sign-up";
    }



    @RestController
    @RequestMapping("/fureverhomes")
    public class MemberRestController{

        //회원가입 정보 전송
        @PostMapping("/signup.post")
        public ResponseEntity<String> signUpMember(@RequestBody MemberReqDto memberReqDto, RedirectAttributes redirectAttributes) {
            if (memberService.saveMember(memberReqDto) != null) {
                System.out.println(ResponseEntity.ok(memberReqDto.getEmail()));
                return ResponseEntity.ok(memberReqDto.getEmail());
            }
            return ResponseEntity.internalServerError().build();
        }
    }
}

정상적으로 Service 계층까지 실행이 된다면 200과 Email 정보를 함께 담아서 응답했다. 이 부분은 다음 포스팅에서 할 이메일 인증과 관련이 있다.

 

 

 

 

signup.html, js

 

 

타임리프를 이용하기 위해서 먼저 그래들에 디펜던시를 추가하고, 타임리프 문법에 맞춰서 html을 수정해주었다. 이전 프로젝트 때는 타임리프 문법에 맞춰서 html을 수정하는게 쉽지가 않아서 제대로 처리하지 못했었는데 이번엔 좀 천천히 알아보면서 main 페이지, 회원가입 페이지를 바꾸어주었다. 

$(document).ready(function() {

    // 정규식 선언
    let regName = RegExp(/^[A-Za-z가-힣 ]+$/);
    let regEmail = RegExp(/^([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/);
    let regPwd = RegExp(/^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,12}$/);

    // 변수 선언
    let emailCheck = true;
    let passwordCheck = true;
    let passwordConfirmCheck = true;
    let nameCheck = true;

    //datepicker
    $("#birthDate").datepicker({
    });

	//이메일 유효성 검사
    $("#email").change(function () {
        if (!regEmail.test($("#email").val())) {
            $(".email").text("유효하지 않은 이메일 양식입니다.").css("color", "red");
            $("#email").attr("class", "form-control is-invalid");
            emailCheck = false;
        } else {
            $(".email").text("");
            $("#email").attr("class", "form-control is-valid");
            emailCheck = true;
        }
    });


	//이름 유효성 검사
    $("#name").change(function () {
        if (!regName.test($("#name").val())) {
            $(".name").text("유효하지 않은 이름 양식입니다.").css("color", "red");
            $("#name").attr("class", "form-control is-invalid");
            nameCheck = false;
        } else {
            $(".name").text("");
            $("#name").attr("class", "form-control is-valid");
            nameCheck = true;
        }
    });

	// 비밀번호 유효성 검사
    $("#password").change(function () {
        let password = $("#password").val();
        if (!regPwd.test(password)) {
            $(".password")
                .text("비밀번호는 영문, 숫자포함 6-12자여야합니다.")
                .css("color", "red");
            $("#password").attr("class", "form-control is-invalid");
            passwordCheck = false;
        } else {
            $(".password").text("");
            $("#password-confirm").focus();
            $("#password").attr("class", "form-control is-valid");
            passwordCheck = true;
        }
    });

	// 비밀번호 일치 여부 검사
    $("#password-confirm").change(function () {
        let password = $("#password").val();
        let confirmedPassword = $("#password-confirm").val();

        if (password !== confirmedPassword) {
            $(".password-confirm")
                .text("비밀번호가 일치하지 않습니다.")
                .css("color", "red");
            $("#password-confirm").attr("class", "form-control is-invalid");
            passwordConfirmCheck = false;
        } else {
            $(".password-confirm").text("");
            $("#password-confirm").focus();
            $("#password-confirm").attr("class", "form-control is-valid");
            passwordConfirmCheck = true;
        }
    });

	//이메일 가입 정보 전송
    $("#createAccount").click(function () {
        if (!emailCheck) {
            $("#email").focus();
            return false;
        } else if (!passwordCheck) {
            $("#password").focus();
            return false;
        } else if (!passwordConfirmCheck) {
            $("#password-confirm").focus();
            return false;
        } else if (!nameCheck) {
            $("#name").focus();
            return false;
        }

        let birthDate = $('#birthDate').val();
        let formattedBirthDate = formatDate(birthDate);

        // 날짜 형식 변환 함수 (MM/dd/yyyy -> yyyy-MM-dd)
        function formatDate(dateString) {
            let parts = dateString.split('/');
            if (parts.length === 3) {
                let month = parts[0];
                let day = parts[1];
                let year = parts[2];
                return `${year}-${month}-${day}`;
            }
            return dateString;
        }

        let data = {
            email: $('input[name="email"]').val(),
            name: $('input[name="name"]').val(),
            password: $('input[name="password"]').val(),
            sex: $('input[name="genderRadios"]:checked').val(),
            birth: formattedBirthDate
        };

        $.ajax({
            type: "POST",
            url: "/fureverhomes/signup.post",
            contentType: 'application/json',
            data: JSON.stringify(data),
            async: false,
            success: function (data, status) {
                let email = btoa(data);
                sessionStorage.setItem("email", email);
                alert("성공! 이메일 인증해주셈");
                location.replace("/fureverhomes/signup/emailAuth");
            },
            error: function (data, textStatus) {
                alert("이미 존재하는 회원입니다. 로그인을 진행해주세요.")
                location.href = "/fureverhomes/signin";
            }
        });
    });
});

ajax를 이용해서 데이터를 넘겨주고 성공시엔 "/fureverhomes/signup/emailAuth"을 통해서 페이지 이동을 하도록 만들 생각이다.

두가지 큰 문제가 있었어서 그 두가지를 정리해보려고 한다.

 

  • LocalDate와 Datepicker 타입 불일치
JSON parse error: Cannot deserialize value of type `java.time.LocalDate` from String "09/18/2023": Failed to deserialize java.time.LocalDate: (java.time.format.DateTimeParseException) Text '09/18/2023' could not be parsed at index 0

클라이언트에서 넘어오는 형식은 "MM/dd/yyyy"이고 LocalDate 타입은 "yyyy-MM-dd"이라서 이 타입을 변경시켜주는 방법을 많이 고민했다. 클라이언트에서 값을 전송할 때부터 포맷을 LocalDate 포맷과 맞도록 변환해서 넘겨주거나, 클라이언트에서는 그대로 전송을 하되 서버에서 파싱해서 LocalDate로 변화할 때 사용되는 날짜 형식을 클라이언트 형식으로 지정하는 방법이 있다.

나는 둘 중에 클라이언트에서 보낼 때부터 formatDate() 함수를 이용해서 포맷을 변경시키는 방법을 택했다. 

 

  • 페이지 이동 불가

ajax를 이용해서 성공 응답을 받게 된다면 location.replace()를 이용해서 이메일 인증 페이지로 이동하기 위해 Controller를 매핑해줬는데 아무리 실행을 해도 페이지가 이동하지 않고, 새로고침이 되면서 url 파라미터 값들이 다 표출됐었다. 비슷한 역할을 수행하는 다른 함수들은 원하는 대로 페이지 이동이 이루어지는데 회원가입 사이트에서만 실행이 안돼서 ajax 내부의 코드를 이리저리 수정해보도 jQuery를 수정해보다가 본질적인 문제가 있지 않을까 해서 html을 다시 살펴보게 되었다.

회원가입 폼을 지정해놓고 회원 가입 버튼을 누르면 submit이 되도록 되어있었고, 자바스크립트에서는 submit이 진행될 때 유효성 검사를 통과하지 못하면 제출이 되지 않도록 지정해뒀다.

 

<수정 전>

$("#signUpForm").submit(function () {
// 유효성 검사 실패시 제출 안되게 하기
if (!emailCheck) {
    $("#email").focus();
    return false;
} else if (!passwordCheck) {
    $("#password").focus();
    return false;
} else if (!passwordConfirmCheck) {
    $("#password-confirm").focus();
    return false;
} else if (!nameCheck) {
    $("#name").focus();
    return false;
}

let birthDate = $('#birthDate').val();
let formattedBirthDate = formatDate(birthDate);

// 날짜 형식 변환 함수 (MM/dd/yyyy -> yyyy-MM-dd)
function formatDate(dateString) {
    let parts = dateString.split('/');
    if (parts.length === 3) {
        let month = parts[0];
        let day = parts[1];
        let year = parts[2];
        return `${year}-${month}-${day}`;
    }
    return dateString;
}

let data = {
    email: $('input[name="email"]').val(),
    name: $('input[name="name"]').val(),
    password: $('input[name="password"]').val(),
    sex: $('input[name="genderRadios"]:checked').val(),
    birth: formattedBirthDate
};

$.ajax({
    type: "POST",
    url: "/fureverhomes/signup.post",
    contentType: 'application/json',
    data: JSON.stringify(data),
    async: false,
    success: function (data, status) {
        let email = btoa(data);
        sessionStorage.setItem("email", email);
        alert("성공! 이메일 인증해주셈");
        location.replace("/fureverhomes/signup/emailAuth");
    },
    error: function (data, textStatus) {
        alert("이미 존재하는 회원입니다. 로그인을 진행해주세요.")
        location.href = "/fureverhomes/signin";
    	}
	});
});

열심히 구글링을 해보다가 나랑 같은 문제를 고민하고 있던 사람의 글을 보게 되었고, 거기서 답을 찾았다!!

버튼 타입이 submit으로 되어있어서 무조건 submit이 됐었던 거다.. html의 버튼 타입을 button으로 바꾸고 jQuery도 submit일 때 함수가 실행되는게 아니라 회원가입 버튼을 누르면 함수가 실행되도록 변경했고, 드디어 페이지 이동에 성공했다!!! 😵

<button type="button" class="btn btn-block btn-warning" id="createAccount">
	회원가입
</button>

 

오늘 이메일 인증 기능까지 구현을 완료했고, 내일 아침에 다음 포스팅에서 정리하도록 하겠다.

728x90