본문 바로가기
Spring Boot

스프링 부트(Spring Boot) 게시판 - 회원가입 기능 구현하기 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

by 도뎡 2023. 5. 9.
반응형
  • 본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.
  • DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기)
  • 화면 처리는 HTML5 기반의 자바 템플릿 엔진인 타임리프(Thymeleaf)를 사용합니다.

이전 글에서는 History API를 이용해서 비동기(Ajax) 페이징 처리의 새로고침 문제를 해결해 보았습니다. 이번을 포함해서 두 차례예 걸쳐 게시판에 회원 기능을 적용해 볼 건데요. 본 글에서는 DB에 회원 데이터를 관리할 테이블을 생성한 후 회원가입 기능을 구현해 보도록 하겠습니다.

 

1. 회원 테이블 칼럼 구조

아래 표는 회원 테이블의 전체 칼럼 구조입니다.

칼럼 설명
id 테이블의 PK(Primary Key)입니다.
login_id 테이블의 UK(Unique Key)입니다. 로그인에 사용되는 ID를 의미하며, 절대 중복되어서는 안됩니다.
password 로그인에 사용되는 비밀번호를 의미합니다.
name 이름을 의미합니다.
gender 성별을 의미합니다.
birthday 생년월일을 의미합니다.
delete_yn 삭제(탈퇴) 여부를 의미합니다.
created_date 생성(가입)일시를 의미합니다.
modified_date 최종 수정일시를 의미합니다.

 

2. 회원 테이블 생성하기

DBMS 툴에서 아래 명령어를 실행해 회원 테이블을 생성해 주세요.

CREATE TABLE `tb_member` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '회원 번호 (PK)',
  `login_id` varchar(20) NOT NULL COMMENT '로그인 ID',
  `password` varchar(60) NOT NULL COMMENT '비밀번호',
  `name` varchar(20) NOT NULL COMMENT '이름',
  `gender` enum('M','F') NOT NULL COMMENT '성별',
  `birthday` date NOT NULL comment '생년월일',
  `delete_yn` tinyint(1) NOT NULL COMMENT '삭제 여부',
  `created_date` datetime NOT NULL DEFAULT current_timestamp() COMMENT '생성일시',
  `modified_date` datetime DEFAULT NULL COMMENT '최종 수정일시',
  PRIMARY KEY (`id`),
  UNIQUE KEY uix_member_login_id (`login_id`)
) COMMENT '회원';

 

생성된 테이블 구조는 다음과 같은데요. gender 칼럼의 타입인 enum은 상수의 개념으로 생각해 주시면 됩니다.

회원 테이블 구조

 

3. 성별 처리용 Enum 클래스 생성하기

회원 테이블의 gender에는 'M(남성)'과 'F(여성)' 둘 중 하나의 값만 저장할 수 있는데요. Java에서도 Enum 클래스를 이용해서 상수를 처리할 수 있습니다. 우선 java 디렉터리에 회원 요청 클래스에서 사용할 다음의 Enum을 추가해 주세요.

package com.study.domain.member;

public enum Gender {

    M, F

}

 

4. 회원 요청(Request) 클래스 생성하기

다음은 회원가입(INSERT)과 회원정보 수정(UPDATE)에 사용될 요청 클래스입니다. 댓글 처리와 마찬가지로 비동기(Ajax) 통신, 즉 JSON 포맷으로 데이터를 주고받기에 @Setter 어노테이션은 필요하지 않습니다.

encodingPassword( )는 비밀번호를 암호화하는 기능으로, 자세한 내용은 뒤에서 설명드리겠습니다.

package com.study.domain.member;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.thymeleaf.util.StringUtils;

import java.time.LocalDate;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberRequest {

    private Long id;                // 회원 번호 (PK)
    private String loginId;         // 로그인 ID
    private String password;        // 비밀번호
    private String name;            // 이름
    private Gender gender;          // 성별
    private LocalDate birthday;     // 생년월일

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

}

 

5. 회원 응답(Response) 클래스 생성하기

다음은 회원 데이터 조회에 사용될 응답 클래스입니다. clearPassword( )는 회원 상세정보를 조회한 후 비밀번호를 초기화하는 용도로 사용되는데요. 자세한 내용은 다음 글에서 설명드리겠습니다.

package com.study.domain.member;

import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalDateTime;

@Getter
public class MemberResponse {

    private Long id;                       // 회원 번호 (PK)
    private String loginId;                // 로그인 ID
    private String password;               // 비밀번호
    private String name;                   // 이름
    private Gender gender;                 // 성별
    private LocalDate birthday;            // 생년월일
    private Boolean deleteYn;              // 삭제 여부
    private LocalDateTime createdDate;     // 생성일시
    private LocalDateTime modifiedDate;    // 최종 수정일시

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

}

 

6. 회원 Mapper 인터페이스 생성하기

다음은 DB와 통신할 MyBatis의 Mapper 인터페이스입니다. 지금까지 구현해 온 게시글, 댓글 CRUD 메서드와 구조가 유사한데요. 상세정보를 조회하는 findByXXX( ) 메서드에서 PK(id)가 아닌 UK(loginId)를 기준으로 쿼리가 실행된다는 차이가 있습니다.

package com.study.domain.member;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberMapper {

    /**
     * 회원 정보 저장 (회원가입)
     * @param params - 회원 정보
     */
    void save(MemberRequest params);

    /**
     * 회원 상세정보 조회
     * @param loginId - UK
     * @return 회원 상세정보
     */
    MemberResponse findByLoginId(String loginId);

    /**
     * 회원 정보 수정
     * @param params - 회원 정보
     */
    void update(MemberRequest params);

    /**
     * 회원 정보 삭제 (회원 탈퇴)
     * @param id - PK
     */
    void deleteById(Long id);

    /**
     * 회원 수 카운팅 (ID 중복 체크)
     * @param loginId - UK
     * @return 회원 수
     */
    int countByLoginId(String loginId);

}

 

7. 회원 XML Mapper 생성하기

MemberMapper 인터페이스와 연결할 MyBatis XML Mapper입니다. src/main/resources/mappers 폴더에 MemberMapper.xml을 추가한 후 SQL 쿼리를 작성해 주세요.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.domain.member.MemberMapper">

    <!-- tb_member 테이블 전체 컬럼 -->
    <sql id="memberColumns">
          id
        , login_id
        , password
        , name
        , gender
        , birthday
        , delete_yn
        , created_date
        , modified_date
    </sql>


    <!-- 회원 정보 저장 (회원가입) -->
    <insert id="save" parameterType="com.study.domain.member.MemberRequest" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO tb_member (
            <include refid="memberColumns" />
        ) VALUES (
              #{id}
            , #{loginId}
            , #{password}
            , #{name}
            , #{gender}
            , #{birthday}
            , 0
            , NOW()
            , NULL
        )
    </insert>


    <!-- 회원 상세정보 조회 -->
    <select id="findByLoginId" parameterType="string" resultType="com.study.domain.member.MemberResponse">
        SELECT
            <include refid="memberColumns" />
        FROM
            tb_member
        WHERE
            delete_yn = 0
            AND login_id = #{value}
    </select>


    <!-- 회원 정보 수정 -->
    <update id="update" parameterType="com.study.domain.member.MemberRequest">
        UPDATE tb_member
        SET
              modified_date = NOW()
            , name = #{name}
            , gender = #{gender}
            , birthday = #{birthday}
            <if test="password != null and password != ''">
            , password = #{password}
            </if>
        WHERE
            id = #{id}
    </update>


    <!-- 회원 정보 삭제 (회원 탈퇴) -->
    <delete id="deleteById" parameterType="long">
        UPDATE tb_member
        SET
            delete_yn = 1
        WHERE
            id = #{id}
    </delete>
    
    
    <!-- 회원 수 카운팅 (ID 중복 체크) -->
    <select id="countByLoginId" parameterType="string" resultType="int">
        SELECT
            COUNT(*)
        FROM
            tb_member
        WHERE
            login_id = #{value}
    </select>

</mapper>

 

8. 스프링 시큐리티 암호화 라이브러리 & 빈(Bean) 추가하기

스프링 시큐리티에서 제공해 주는 PasswordEncoder를 이용하면 비밀번호를 쉽게 암호화(Encoding) 할 수 있습니다. 비밀번호는 절대로 복호화(Decoding), 즉 암호화되기 전 상태로 돌아가서는 안 되는데요. PasswordEncoder는 복호화가 불가능한 단방향 Hash 알고리즘을 제공해 주는 인터페이스입니다. 자세한 내용은 서비스 처리 과정에서 설명드리겠습니다.


8-1) spring-security-crypto 라이브러리 추가하기

PasswordEncoder를 이용하려면 스프링 시큐리티의 crypto 라이브러리가 필요합니다. 우선 build.gradle에 다음의 코드를 추가한 후 인텔리제이 우측 상단의 코끼리를 클릭해서 그레이들을 리로드해 주세요.

implementation group: 'org.springframework.security', name: 'spring-security-crypto', version: '5.7.6'  /* spring-security-crypto */

 

8-2) PassowrdEncoder 빈(Bean) 추가하기

이제 PasswordEncoder를 스프링 컨테이너에 빈(Bean)으로 등록해 주어야 합니다. java 디렉터리에 아래 클래스를 추가해 주세요.

package com.study.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

9. 회원 서비스(Service) 클래스 생성하기

다음은 비즈니스 로직을 담당하는 서비스 클래스입니다.

package com.study.domain.member;

import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@Service
@RequiredArgsConstructor
public class MemberService {

    private final MemberMapper memberMapper;
    private final PasswordEncoder passwordEncoder;

    /**
     * 회원 정보 저장 (회원가입)
     * @param params - 회원 정보
     * @return PK
     */
    @Transactional
    public Long saveMember(final MemberRequest params) {
        params.encodingPassword(passwordEncoder);
        memberMapper.save(params);
        return params.getId();
    }

    /**
     * 회원 상세정보 조회
     * @param loginId - UK
     * @return 회원 상세정보
     */
    public MemberResponse findMemberByLoginId(final String loginId) {
        return memberMapper.findByLoginId(loginId);
    }

    /**
     * 회원 정보 수정
     * @param params - 회원 정보
     * @return PK
     */
    @Transactional
    public Long updateMember(final MemberRequest params) {
        params.encodingPassword(passwordEncoder);
        memberMapper.update(params);
        return params.getId();
    }

    /**
     * 회원 정보 삭제 (회원 탈퇴)
     * @param id - PK
     * @return PK
     */
    @Transactional
    public Long deleteMemberById(final Long id) {
        memberMapper.deleteById(id);
        return id;
    }


    /**
     * 회원 수 카운팅 (ID 중복 체크)
     * @param loginId - UK
     * @return 회원 수
     */
    public int countMemberByLoginId(final String loginId) {
        return memberMapper.countByLoginId(loginId);
    }

}

 

구성 요소 설명
passwordEncoder SecurityConfig에 선언한 PasswordEncoder 빈(Bean)입니다.
saveMember( ) 회원 정보를 저장합니다. MemberRequest의 encodingPassword( )를 호출해서 비밀번호를 암호화 하는데요. PasswordEncoder의 encode( )는 파라미터로 전달한 값을 60 자리의 난수로 리턴해 줍니다. 회원 테이블의 password를 varchar(60) 자리로 선언한 이유도 이와 같습니다.
findMemberByLoginId( ) 로그인 ID를 기준으로 회원 상세정보를 조회합니다.
updateMember( ) 회원 정보를 수정합니다. saveMember( )와 실행되는 쿼리에만 차이가 있습니다.
deleteMemberById( ) PK를 기준으로 회원 정보를 삭제합니다. 게시글, 댓글과 마찬가지로 논리 삭제 방식을 이용합니다.
countMemberByLoginId( ) 로그인 ID를 기준으로 회원 수를 카운팅합니다. 아이디 중복을 체크하는 용도로 사용됩니다.

 

10. 회원 컨트롤러(Controller) 클래스 생성하기

마지막으로 회원 관련 페이지와 데이터의 CRUD를 처리할 컨트롤러입니다. 댓글과는 달리 회원가입 페이지가 필요하기 때문에 @RestController가 아닌 @Controller와 @ResponseBody가 사용되었습니다.

package com.study.domain.member;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    // 로그인 페이지
    @GetMapping("/login.do")
    public String openLogin() {
        return "member/login";
    }

    // 회원 정보 저장 (회원가입)
    @PostMapping("/members")
    @ResponseBody
    public Long saveMember(@RequestBody final MemberRequest params) {
        return memberService.saveMember(params);
    }

    // 회원 상세정보 조회
    @GetMapping("/members/{loginId}")
    @ResponseBody
    public MemberResponse findMemberByLoginId(@PathVariable final String loginId) {
        return memberService.findMemberByLoginId(loginId);
    }

    // 회원 정보 수정
    @PatchMapping("/members/{id}")
    @ResponseBody
    public Long updateMember(@PathVariable final Long id, @RequestBody final MemberRequest params) {
        return memberService.updateMember(params);
    }

    // 회원 정보 삭제 (회원 탈퇴)
    @DeleteMapping("/members/{id}")
    @ResponseBody
    public Long deleteMemberById(final Long id) {
        return memberService.deleteMemberById(id);
    }

    // 회원 수 카운팅 (ID 중복 체크)
    @GetMapping("/member-count")
    @ResponseBody
    public int countMemberByLoginId(@RequestParam final String loginId) {
        return memberService.countMemberByLoginId(loginId);
    }

}

 

11. 로그인 페이지(HTML) 처리하기


11-1) 회원(member) 디렉터리 & HTML 생성하기

MemberController의 openLogin( )에서 리턴하는 페이지를 처리할 차례입니다. 아래와 같이 resources 디렉터리에 member 폴더와 login.html을 추가해 주세요.

member 디렉터리 구조

 

11-2) HTML 소스 코드 작성하기

login.html에 다음의 코드를 작성해 주시면 됩니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    <title>로그인</title>
    <link rel="stylesheet" th:href="@{/css/default.css}" />
    <link rel="stylesheet" th:href="@{/css/common.css}" />
    <link rel="stylesheet" th:href="@{/css/content.css}" />
    <link rel="stylesheet" th:href="@{/css/button.css}" />
	<style>
		#login_box .signup_btn {background:#42d870; border:0; border-bottom:solid 3px #4ed177; border-radius:50px; width:100%; height:52px; line-height:52px; font-size:16px; color:#fff; text-align:center; margin:20px 0 15px;}
	</style>
</head>

<body>
	<div id="login_wrap">
		<div id="login_box">
			<div class="login_con">
				<div class="login_tit">
					<h2>게시판 프로젝트</h2>
					<p>LOG<i>IN</i></p>
				</div>

				<div class="login_input">
					<form id="loginForm" onsubmit="return false;" autocomplete="off">
						<ul>
							<li>
								<label for="loginId" class="skip_info">아이디</label>
								<input type="text" id="loginId" name="loginId" placeholder="아이디" title="아이디" />
							</li>
							<li>
								<label for="password" class="skip_info">비밀번호</label>
								<input type="password" id="password" name="password" title="비밀번호" placeholder="비밀번호" />
							</li>
						</ul>
						<button type="button" onclick="login();" class="login_btn">로그인</button>
						<button type="button" onclick="openSignupPopup();" class="signup_btn">회원가입</button>
					</form>
				</div>
			</div>
		</div>
	</div>

	<!--/* 회원가입 popup */-->
	<div id="signupPopup" class="popLayer">
		<h3>회원가입</h3>
		<div class="pop_container">
			<form id="signupForm" onsubmit="return false;" autocomplete="off">
				<table class="tb tb_row tl">
					<colgroup>
						<col style="width:30%;" /><col style="width:70%;" />
					</colgroup>
					<tbody>
						<tr>
							<th scope="row">아이디<span class="es">필수 입력</span></th>
							<td>
								<input type="text" name="loginId" placeholder="아이디" maxlength="20" style="width: 80%;" />
								<button type="button" id="idCheckBtn" class="btns btn_st5" onclick="checkLoginId();" style="width: 20%; float: right;">중복 확인</button>
							</td>
						</tr>
						<tr>
							<th scope="row">비밀번호<span class="es">필수 입력</span></th>
							<td><input type="password" name="password" placeholder="비밀번호" maxlength="30" /></td>
						</tr>
						<tr>
							<th scope="row">비밀번호 재입력<span class="es">필수 입력</span></th>
							<td><input type="password" name="passwordCheck" placeholder="비밀번호 재입력" maxlength="30" /></td>
						</tr>
						<tr>
							<th scope="row">이름<span class="es">필수 입력</span></th>
							<td><input type="text" name="name" placeholder="이름" maxlength="10" /></td>
						</tr>
						<tr>
							<th scope="row">성별<span class="es">필수 입력</span></th>
							<td>
								<div class="radio_group">
									<p class="radios">
										<input type="radio" id="male" name="gender" value="M" checked />
										<label for="male">남</label><span class="check"></span>
									</p>
									<p class="radios">
										<input type="radio" id="female" name="gender" value="F" />
										<label for="female">여</label><span class="check"></span>
									</p>
								</div>
							</td>
						</tr>
						<tr>
							<th scope="row">생년월일<span class="es">필수 입력</span></th>
							<td><input type="number" name="birthday" placeholder="숫자 8자리 입력" /></td>
						</tr>
					</tbody>
				</table>
			</form>
			<p class="btn_set">
				<button type="button" class="btns btn_st2" onclick="saveMember();">가입</button>
				<button type="button" class="btns btn_bdr2" onclick="closeSignupPopup();">취소</button>
			</p>
		</div>
		<button type="button" class="btn_close" onclick="closeSignupPopup();"><span><i class="far fa-times-circle"></i></span></button>
	</div>

	<script th:src="@{/js/function.js}"></script>
	<script th:src="@{/js/jquery-3.6.0.min.js}"></script>
	<script th:src="@{/js/common.js}"></script>
	<script src="https://kit.fontawesome.com/79613ae794.js" crossorigin="anonymous"></script>
	<script>

		// 회원가입 팝업 open
		function openSignupPopup() {
			layerPop('signupPopup')
		}


		// 회원가입 팝업 close
		function closeSignupPopup() {
			const form = document.getElementById('signupForm');
			form.loginId.readOnly = false;
			form.querySelector('#idCheckBtn').disabled = false;
			form.reset();
			layerPopClose();
		}


		// 아이디 중복 체크
		function checkLoginId() {
			const loginId = document.querySelector('#signupForm input[name="loginId"]');
			isValid(loginId, '아이디');
			const count = getJson(`/member-count`, { loginId : loginId.value });

			if (count > 0) {
				alert('이미 가입된 아이디가 있습니다.');
				loginId.focus();
				return false;
			}

			if (confirm('사용 가능한 아이디입니다.\n입력하신 아이디로 결정하시겠어요?')) {
				loginId.readOnly = true;
				document.getElementById('idCheckBtn').disabled = true;
			}
		}


		// 회원 정보 유효성 검사
		function validationMemberInfo(form) {

			const fields = form.querySelectorAll('input:not([type="radio"])');
			const fieldNames = ['아이디', '비밀번호', '빕밀번호 재입력', '이름', '생년월일'];

			for (let i = 0, len = fields.length; i < len; i++) {
				isValid(fields[i], fieldNames[i]);
			}

			if (form.loginId.readOnly === false) {
				alert('아이디 중복 체크를 완료해 주세요.');
				throw new Error();
			}

			if (form.password.value !== form.passwordCheck.value) {
				alert('비밀번호가 일치하지 않습니다.');
				form.passwordCheck.focus();
				throw new Error();
			}
		}


		// 회원 정보 저장 (회원가입)
		function saveMember() {

			// 1. 필드 유효성 검사
			const form = document.getElementById('signupForm');
			validationMemberInfo(form);

			// 2. 파라미터 세팅
			const params = {}
			new FormData(form).forEach((value, key) => params[key] = value.trim());
			params.birthday = params.birthday.replace(/(\d{4})(\d{2})(\d{2})/g, '$1-$2-$3');

			// 3. Save API 호출
			callApi('/members', 'post', params);
			alert('가입을 축하드립니다!\n로그인 후 서비스를 이용해 주세요.');
			closeSignupPopup();
		}

	</script>
</body>
</html>

 

로그인을 처리하는 loginForm은 다음 글에서 설명드릴 예정이니, 여기서는 회원가입 레이어 팝업과 JavaScript 함수를 위주로 설명드리겠습니다.

구성 요소 설명
signupPopup 회원가입 페이지를 따로 구성하지 않고 레이어 팝업을 이용해서 회원가입을 처리합니다. 댓글 처리에 사용된 레이어 팝업과 동일하며, 필드 개수에만 차이가 있습니다.
openSignupPopup( ) 회원가입 팝업을 띄우는 함수입니다. common.js의 layerPop( )을 호출하는 게 전부입니다.
closeSignupPopup( ) signupForm 안에 있는 로그인 ID의 읽기 모드(readOnly), 중복 확인 버튼의 비활성화(disabled) 속성, 그리고 모든 필드의 값을 초기화 한 후 common.js의 layerPopClose( )를 호출해 팝업을 닫는 함수입니다.

로그인 ID의 readOnly와 중복 확인 버튼의 disbled를 초기화하는 이유는 아래 함수에서 설명드리겠습니다.
checkLoginId( ) 로그인 ID의 중복을 체크하는 함수입니다. MemberController의 countMemberByLoginId( )를 호출해서 사용자가 입력한 ID와 동일한 아이디가 있으면 로직을 종료하고, 없는 경우에는 아이디 결정 여부를 물어 "확인(true)"이 떨어지면 로그인 ID와 중복 확인 버튼을 각각 읽기 모드(readOnly), 비활성화(disabled) 처리합니다.

closeSignupPopup( )에서 로그인 ID와 중복 확인 버튼을 초기화하는 이유는 이 때문입니다.
validationMemberInfo( ) 회원 정보의 유효성을 검사하는 함수입니다. 변수 fields는 파라미터로 전달받은 signupForm의 모든 필드 중 성별(radio) 버튼을 제외한 모든 필드를 의미하고, fieldNames는 fields에 해당되는 필드명을 의미합니다.

사실, 정규식을 이용해서 좀 더 디테일하게 로직을 작성할 필요가 있으나, 설명이 길어질 듯하여 필수 입력 정도만 처리했습니다. isValid( )는 function.js에 선언된 공통 유효성 검사 함수입니다.
saveMember( ) MemberController의 saveMember( )를 호출해서 DB에 회원 정보를 저장하는 함수입니다. 파라미터를 세팅하는 2번만 설명을 드리면 될 듯합니다.

폼 데이터(FormData)의 생성자에 form 엘리먼트를 전달하면, form 안에 있는 모든 필드의 name과 value가 FormData 객체에 key-value 형식으로 저장됩니다. 이 FormData 객체를 forEach( )로 순환해서 리터럴 객체인 params에 key-value를 저장하고, 생년월일(birthday)을 yyyy-MM-dd 형식으로 치환(replace)한 후 Save API를 호출합니다.

파라미터를 직접 세팅해도 되지만, 필드 개수가 많은 경우에는 그만큼 코드가 길어지게 됩니다. FormData를 잘 활용하면 이와 같이 필드의 개수에 상관없이 파라미터를 쉽게 세팅할 수 있으며, FormData는 비동기(Ajax) 파일 업로드에도 사용됩니다.

 

12. 회원가입 테스트하기

모든 준비는 끝났으니 회원가입 기능을 테스트해 보겠습니다.


12-1) 로그인 페이지

가장 먼저 MemberController의 openLogin( )이 리턴하는 로그인 페이지입니다.

로그인 페이지 구조

 

12-2) 회원가입 팝업

이번에는 회원가입 레이어 팝업입니다.

회원가입 팝업 구조

 

다음은 가입 가능한 아이디를 입력한 후 중복 확인을 클릭한 경우입니다. 취소를 누르면 로직이 종료되고, 확인을 누르면 아이디 필드는 읽기 모드(readOnly)가 되며, 중복 확인 버튼은 비활성화(disabled)됩니다.

중복 확인 - 가입 가능

 

이번에는 중복되는 아이디가 있는 경우입니다.

중복 확인 - 가입 불가능

 

마지막으로 회원가입에 성공하면 "가입 축하" 메시지가 열리고, 창을 닫으면 팝업이 종료됩니다.

회원가입 완료

 

마치며

이렇게 회원가입 기능 구현이 완료되었습니다. 다음 글에서는 로그인/로그아웃 기능을 구현해 볼 건데요. 인터셉터를 이용해서 로그인이 되지 않은 경우, 즉 세션이 없는 경우에는 로그인 페이지에만 접근이 가능하도록 처리하게 됩니다.

사실, 회원 기능은 스프링 시큐리티(Spring Security)를 이용해서 처리하는 게 베스트이긴 합니다만, 시큐리티는 학습 곡선(Learning curve)이 높기 때문에 인터셉터로 페이지 접근을 컨트롤해 보고, 나중에 JPA와 QueryDSL 기반의 프로젝트를 진행할 때 시큐리티를 곁들여 볼까 합니다.

스마일 라식을 한 지 2주 정도가 지났으나 아직 시력이 회복되지 않아 본 글을 작성하는 데까지 오랜 시간이 걸렸습니다.. 기다려 주신 분들께 정말 죄송스럽고, 오늘도 방문해 주셔서 진심으로 감사드립니다! 좋은 하루 보내세요 :)

Board.zip
0.90MB

반응형

댓글