약 2년 전에 만들었던 게시판 사이트 소스 코드를 살펴보았는데, 소스 코드가 뒤죽박죽 섞여있고 기초적인 부분에서도 부족한 점이 많이 보여 이참에 다시 작성하기로 했다.
이번 글에서는 회원가입 및 로그인 기능을 구현하는 코드에 대하여 설명하겠다. 모든 디자인은 부트스트랩을 사용했다.
1. 부트스트랩 및 jQuery 스크립트 태그 추가
파일 최상단 또는 Head 파일에 다음과 같이 작성하여 부트스트랩 및 jQuery를 사용하기 위해 추가한다.
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.6.1.js" integrity="sha256-3zlB5s2uwoUzrXK3BT7AX3FyvojsraNFxCc2vC/7pNI=" crossorigin="anonymous"></script>
2. 로그인 폼 작성
Index 파일은 로그인 폼으로 작성한다. 로그인 폼 이외에도 비밀번호 찾기와 회원가입으로 이동할 수 있는 <a>태그를 작성한다.
<form name="login_form" action="로그인 처리 파일 경로" method="post"> <div class="loginwrap"> <img src="로고 이미지 경로" class="img-responsive" ><br><br> <div class="form-floating mb-3"> <input type="text" name="id" class="form-control" id="floatingID" autofocus="true" required="true" placeholder="ID" autocomplete="off"> <label for="floatingID">아이디</label> </div> <div class="form-floating"> <input type="password" name="password" class="form-control" id="floatingPassword" required="true" placeholder="Password"> <label for="floatingPassword">비밀번호</label> </div><br> <button class="btn btn-primary form-control" id="signup_btn">로그인</button><br><br> <div style="text-align:right;"> <a href="비밀번호 찾기 폼 경로">비밀번호 찾기</a> | <a href="회원가입 폼 경로">회원가입</a> </div> </div> </form> <script type="text/javascript" src="동작 구현 파일 경로"></script>
3. 회원가입 폼 작성
회원가입 폼에 각 회원 정보들을 입력할 수 있게 작성한다. 아이디에 한글 입력 시, 생년월일과 전화번호에 숫자 이외 입력 시 스크립트로 문자를 제거한다. 아이디 중복을 방지하기 위해 아이디 중복 확인 버튼을 작성하고 주소 검색 버튼 클릭 시 카카오 주소 검색 API를 사용하기 위해 스크립트 코드도 추가한다.
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script> <form id="member_register_frm" method="post" action="회원가입 처리 파일 경로" enctype="multipart/form-data"> <div class="signwrap"><br> <img src="로고 이미지 경로" class="img-responsive" ><br><hr><br> <table class="table table-borderless rounded-3 overflow-hidden"> <tbody> <tr> <th>아이디 <span style="color:red;">*</span></th> <td class="form-group row"> <input type="text" id="member_id" name="member_id" placeholder="2~12자 이내 영문과 숫자" class="form-control" oninput="this.value=this.value.replace(/[^a-zA-Z-_0-9]/g,'');"> <button type="button" id="id_check_btn" class="btn btn-secondary">아이디 중복 확인</button> </td> </tr> <tr> <th>비밀번호 <span style="color:red;">*</span></th> <td class="form-group row"> <input type="password" id="member_password" name="member_password" placeholder="4자 이상 영문과 숫자" class="form-control"> </td> </tr> <tr> <th>이름 <span style="color:red;">*</span></th> <td class="form-group row"> <input type="text" id="member_first_name" name="member_first_name" placeholder="이름" class="form-control"> <input type="text" id="member_last_name" name="member_last_name" placeholder="성" class="form-control"> </td> </tr> <tr> <th>생년월일<span style="color:red;">*</span></th> <td class="form-group row"> <input type="text" id="member_birthY" name="member_birthY" placeholder="년(4자)" class="form-control" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');"> <select name="member_birthM" class="form-select"> <option value="">월</option> <?php for($i=1; $i<13; $i++){ if($i < 10) $ival = "0".$i; ?> <option value="<?=$ival?>"><?=$i?></option> <?php } ?> </select> <input type="text" id="member_birthD" name="member_birthD" placeholder="일" class="form-control" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');"> </td> </tr> <tr> <th>전화번호 <span style="color:red;">*</span></th> <td class="form-group row"> <input type="text" id="member_phone" name="member_phone" placeholder="전화번호 입력" class="form-control" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');"> </td> </tr> <tr> <th>주소 / 상세주소 *</th> <td class="form-group row"> <input type="text" name="member_address" id="member_address" placeholder="도로명 또는 지번" class="form-control"> <input type="text" name="member_detail_address" id="member_detail_address" placeholder="ex) 101동 1001호" class="form-control"> <button type="button" id="postBtn" class="btn btn-secondary">검색</button> </td> </tr> <tr> <th>이메일 *</th> <td class="form-group row"> <input type="text" id="member_email" name="member_email" placeholder="이메일" class="form-control"> <select name="member_email_select" class="form-select"> <option value="N">직접 입력</option> <option value="naver.com">@naver.com</option> <option value="hanmail.net">@hanmail.net</option> <option value="daum.net">@daum.net</option> <option value="nate.com">@nate.com</option> <option value="gmail.com">@gmail.com</option> <option value="yahoo.com">@yahoo.com</option> </select> </td> </tr> </tbody> </table><br> <div class="pagination justify-content-center"> <button type="button" id="member_save" class="btn btn-primary">회원가입</button> </div> </div> </form> <script type="text/javascript" src="동작 구현 파일 경로"></script>
4. 회원 DB 생성 및 설정
회원가입 시 회원 정보가 저장되는 DB를 생성한다.
CREATE TABLE `member` ( `no` INT(11) NOT NULL AUTO_INCREMENT, `member_id` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_password` VARCHAR(300) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_first_name` VARCHAR(20) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_last_name` VARCHAR(20) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_birthY` VARCHAR(5) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_birthM` VARCHAR(5) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_birthD` VARCHAR(5) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_phone` VARCHAR(15) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_address` VARCHAR(200) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_detail_address` VARCHAR(200) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_email` VARCHAR(80) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_email_select` VARCHAR(30) NULL DEFAULT NULL COLLATE 'utf8_general_ci', `member_joindate` DATETIME NULL DEFAULT NULL, PRIMARY KEY (`no`) USING BTREE, UNIQUE INDEX `member_id` (`member_id`) USING BTREE ) COLLATE='utf8_general_ci' ENGINE=InnoDB AUTO_INCREMENT=1 ;
5. 비밀번호 찾기 폼 작성
비밀번호 찾기 폼은 간단하게 회원가입 시 입력했던 아이디와 전화번호로 본인확인 후 비밀번호를 초기화하고 재설정할 수 있도록 작성한다. 회원가입 폼과 같이 아이디와 전화번호는 스크립트로 문자 형식에 제한을 둔다.
본인확인이 완료되었다면 비밀번호 찾기 폼은 비밀번호 재설정 폼으로 변경된다.
<form id="member_find_frm" method="post" action="비밀번호 찾기 처리 파일 경로" enctype="multipart/form-data"> <div class="findwrap"><br> <img src="로고 이미지 경로" class="img-responsive" ><br> <?php if($_GET["find_status"] == "confirmed" && !empty($_GET["member_no"])){ echo "새로운 비밀번호를 설정하세요."; }else{ echo "가입 시 입력한 아이디와 전화번호로 본인확인 후 비밀번호를 다시 설정할 수 있습니다."; } ?> <br><hr><br> <table class="table table-borderless rounded-3 overflow-hidden"> <tbody> <?php if($_GET["find_status"] == "confirmed" && !empty($_GET["member_no"])){ ?> <tr> <th>비밀번호 <span style="color:red;">*</span></th> <td class="form-group row"> <input type="password" id="member_password_find" name="member_password_find" placeholder="4자 이상 영문과 숫자" class="form-control"> <input type = "hidden" id="member_no" name = "member_no" value="<?=$_GET["member_no"]?>"> </td> </tr> <?php }else{ ?> <tr> <th>아이디 <span style="color:red;">*</span></th> <td class="form-group row"> <input type="text" id="member_id_find" name="member_id_find" placeholder="아이디 입력" class="form-control" oninput="this.value=this.value.replace(/[^a-zA-Z-_0-9]/g,'');"> </td> </tr> <tr> <th>전화번호 <span style="color:red;">*</span></th> <td class="form-group row"> <input type="text" id="member_phone_find" name="member_phone_find" placeholder="전화번호 입력" class="form-control" oninput="this.value = this.value.replace(/[^0-9.]/g, '').replace(/(\..*)\./g, '$1');"> </td> </tr> <?php } ?> </tbody> </table><br> <div class="pagination justify-content-center"> <?php if($_GET["find_status"] == "confirmed" && !empty($_GET["member_no"])){ ?> <button type="button" id="member_reset_pwd" class="btn btn-danger">비밀번호 재설정</button> <?php }else{ ?> <button type="button" id="member_find" class="btn btn-primary">비밀번호 찾기</button> <?php } ?> </div> </div> </form> <script type="text/javascript" src="동작 구현 파일 경로"></script>
6. 동작 구현 파일 작성
아이디 중복 유무를 확인해서 AJAX로 중복 유무를 받아오고 사용 가능한 아이디라면 중복 확인이 완료된 상태로 변경한다.
주소 검색 버튼을 클릭하면 카카오 주소 검색창을 띄운 후 검색한 주소가 칸에 입력되게 구현하고, 폼에서 전송되는 모든 값에 대한 빈값 검사를 한다.
$("#id_check_btn").click(function(){ var id = $("#member_id").val(); if(id.length > 0){ if(id.length < 2 || id.length > 12){ alert("아이디는 2자 이상 12자 이하로 입력해 주세요."); return false; } $.ajax({ url: "아이디 중복 확인 처리 파일 경로", type: "POST", data: "id=" + id, success: function(data){ data = data.trim(); if(data == 0){ alert("사용가능한 아이디입니다."); $("#member_id").attr("state", "Y"); }else{ alert("이미 사용중인 아이디입니다."); $("#member_id").attr("state", "N"); } } }); }else{ alert("아이디를 입력해 주세요."); return false; } }); $("#postBtn").click(function(){ new daum.Postcode({ oncomplete: function(data){ $("#member_address").val(data.roadAddress); $("#member_detail_address").focus(); } }).open(); }); $("#member_save").click(function(){ if($("#member_id").val() == ""){ alert("아이디를 입력해 주세요."); return false; } if($("#member_id").attr("state") != "Y"){ alert("아이디 중복 확인을 해주세요."); return false; } if($("#member_password").val() == ""){ alert("비밀번호를 입력해 주세요."); return false; } if($("#member_first_name").val() == ""){ alert("이름을 입력해 주세요."); return false; } if($("#member_last_name").val() == ""){ alert("성을 입력해 주세요."); return false; } var now = new Date(); var year= now.getFullYear(); if($("#member_birthY").val() == "" || $("#member_birthY").val() > year || $("#member_birthY").val() < 1900){ alert("생년월일을 정확히 입력해 주세요."); return false; } if($("#member_birthM option:selected").val() == ""){ alert("생년월일을 정확히 입력해 주세요."); return false; } if($("#member_birthD").val() == "" || $("#member_birthD").val() > 31){ alert("생년월일을 정확히 입력해 주세요."); return false; } if($("#member_phone").val() == ""){ alert("전화번호를 입력해 주세요."); return false; } $("#member_register_frm").submit(); }); $("#member_find").click(function(){ if($("#member_id_find").val() == ""){ alert("아이디를 입력해 주세요."); return false; } if($("#member_phone_find").val() == ""){ alert("전화번호를 입력해 주세요."); return false; } $("#member_find_frm").submit(); }); $("#member_reset_pwd").click(function(){ if($("#member_password_find").val() == ""){ alert("비밀번호를 입력해 주세요."); return false; } $("#member_find_frm").submit(); });
7. 아이디 중복 확인 처리 파일 작성
회원이 입력한 아이디가 DB에 이미 존재하는지 확인해서 아이디 중복 유무를 확인한다.
<?php global $mysqli; $sql = "select no from member where member_id = '".$_POST["id"]."'"; $result = $mysqli->query($sql); echo $result->num_rows; ?>
8. 회원가입 및 비밀번호 찾기 처리 파일 작성
회원가입 시 값이 모두 잘 들어왔는지 확인 후 비밀번호는 PHP 내장 함수 password_hash()를 사용해서 암호화하고 DB에 데이터를 저장한다. 회원가입이 완료되었다면 로그인 페이지로 이동한다.
비밀번호 찾기 페이지에서 본인인증 시 아이디와 전화번호 값을 확인하고 일치한다면 비밀번호 재설정 페이지로 이동한다. 이동 후 회원이 비밀번호를 재설정하면 입력한 비밀번호를 다시 암호화해서 비밀번호 데이터를 업데이트한다.
<?php global $mysqli; if(!empty($_POST["member_id"]) && !empty($_POST["member_password"]) && !empty($_POST["member_first_name"]) && !empty($_POST["member_last_name"]) && !empty($_POST["member_birthY"]) && !empty($_POST["member_birthM"]) && !empty($_POST["member_birthD"]) && !empty($_POST["member_phone"])){ $_POST["member_password"] = password_hash($_POST["member_password"], PASSWORD_DEFAULT); if((int)$_POST["member_birthD"] < 10){ $_POST["member_birthD"] = "0".$_POST["member_birthD"]; } $column = ""; $values = ""; foreach($_POST as $key => $value){ $column .= $key.", "; $values .= "'".$value."', "; } $sql = "insert into member (".$column." member_joindate) values (".$values." now())"; $result = $mysqli->query($sql); if($result){ echo "<script>alert('회원가입에 성공하였습니다.');location.replace('로그인 페이지 경로');</script>"; }else{ echo "<script>alert('문제가 발생했습니다. 관리자에게 문의해 주세요.');</script>"; } } if(!empty($_POST["member_id_find"]) && !empty($_POST["member_phone_find"])){ $sql = "select no from member where member_id = '".$_POST["member_id_find"]."' and member_phone = '".$_POST["member_phone_find"]."'"; $result = $mysqli->query($sql); if($result){ $row = $result->fetch_array(); echo "<script>alert('본인확인에 성공하였습니다.');location.replace('비밀번호 찾기 페이지 경로?find_status=confirmed&member_no=".$row["no"]."');</script>"; }else{ echo "<script>alert('아이디 또는 전화번호를 다시 확인해 주세요.');</script>"; } } if(!empty($_POST["member_password_find"]) && !empty($_POST["member_no"])){ $_POST["member_password_find"] = password_hash($_POST["member_password_find"], PASSWORD_DEFAULT); $sql = "update member set member_password = '".$_POST["member_password_find"]."' where no = '".$_POST["member_no"]."'"; $result = $mysqli->query($sql); if($result){ echo "<script>alert('비밀번호 재설정에 성공하였습니다.');location.replace('로그인 페이지 경로');</script>"; }else{ echo "<script>alert('문제가 발생했습니다. 관리자에게 문의해 주세요.');</script>"; } } ?>
회원가입과 비밀번호 찾기가 잘 동작하는지 확인한다.
9. 로그인 처리 파일 작성
로그인 시 DB에 저장된 아이디와 암호화된 비밀번호가 입력한 정보와 같으면 커뮤니티 메인으로 이동한다. 아이디가 존재하지 않거나 비밀번호가 틀렸을 경우 예외 처리한다. 로그인이 완료되었다면 SESSION에 아이디와 이름을 저장한다.
<?php try{ if(isset($_POST["id"]) && isset($_POST["password"])){ global $mysqli; $sql = "select * from member where member_id = '".$_POST["id"]."'"; if(!$result = $mysqli->query($sql)){throw new Exception("Not permitted to log in/connect.");} if($result->num_rows == 0){ throw new Exception("등록되지 않은 아이디입니다."); }else if($result->num_rows == 1){ $row = $result->fetch_assoc(); if(password_verify($_POST["password"], $row["member_password"])){ session_start(); $_SESSION["id"] = $row["member_id"]; $_SESSION["name"] = $row["member_last_name"].$row["member_first_name"]; echo "<script>location.replace('로그인 후 이동 경로');</script>"; exit; }else{ throw new Exception("아이디 또는 비밀번호가 맞지 않습니다."); } }else{ exit; } } }catch(Exception $e){ echo "<script>alert('".$e->getMessage()."');history.back();</script>"; } ?>
10. 로그아웃 처리 및 로그인 확인 파일 작성
로그인이 잘 되었는지 확인하기 위해 로그인 후 이동하는 페이지에 저장했던 SESSION 값을 출력한다. 이 페이지는 후에 게시판 메인 페이지로 쓰일 예정이다. 여기에 로그아웃 버튼도 간단하게 작성한다.
<?php echo $_SESSION['id']."[".$_SESSION['name']."]"; ?> <form method="post" action="로그아웃 처리 파일 경로" id="logoutfrm"> <button class="btn btn-light">로그아웃 <i class="bi bi-box-arrow-right"></i></button> </form>
<?php session_destroy(); echo "<script>alert('로그인 화면으로 돌아갑니다.');location.replace('로그인 페이지 경로');</script>"; if($mysqli){$mysqli->close();} ?>
로그인 후 페이지 이동, SESSION 값 출력, 로그아웃 모두 잘 되는지 확인한다.