Bigfat

[Web] 로그인, 로그아웃 구현하기 (게시판 구현) 본문

Java/web

[Web] 로그인, 로그아웃 구현하기 (게시판 구현)

kyou 2017. 4. 11. 14:39

로그인 화면을 만들고, 로그인, 로그아웃 기능을 구현해보자

  게시판(BBS)의 첫 화면이 될 로그인 화면을 만들고, 로그인, 로그아웃 기능을 구현해보도록 한다. 이전에 만들어둔 화면들의 수정이 필요해 코드가 많고, 제법 길다.


  우선 web.xml에서 <welcome-file>의 값을 login.jsp로 작성하자. 이제 http://localhost/bbs로 접근하면 login.jsp화면을 보여준다. 가장 하단의 <session-config>에서 세션에 대한 설정을 할 수 있다. <session-timeout>의 기준은 분(minute)이며, 기본 값은 30분이다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>bbs</display-name>
  <welcome-file-list>
    <welcome-file>login.jsp</welcome-file>
  </welcome-file-list>
  
  <!-- 프로젝트에 사용되는 파라미터 -->
  <!-- application 내장 객체가 읽어들인다. -->
  <context-param>
	  <param-name>pageSize</param-name>
	  <param-value>10</param-value>
  </context-param>
  <context-param>
	  <param-name>pageBlock</param-name>
	  <param-value>10</param-value>
  </context-param>
  
  <servlet>
    <servlet-name>bbsServlet</servlet-name>
    <servlet-class>com.edu.bbs.BBSServlet</servlet-class>
    <init-param>
      <param-name>bbsProperties</param-name>
      <param-value>D:\Dev\education\bbs\WebContent\bbs.properties</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
    <servlet-name>bbsServlet</servlet-name>
    <url-pattern>*.bbs</url-pattern>
  </servlet-mapping>
  
  <!-- 에러 페이지 처리 -->
  <error-page>
  	<error-code>404</error-code>
  	<location>/errorPage.jsp</location>
  </error-page>
  <error-page>
  	<error-code>500</error-code>
  	<location>/errorPage.jsp</location>
  </error-page>
  
  <!-- 세션 설정 -->
  <session-config>
  	<session-timeout>10</session-timeout>
  </session-config>
</web-app>


  bbs.properties파일에 /login.bbs와 /logout.bbs를 클래스들과 매핑시켜 주도록 작성한다.

#bbs.properties
/writeForm.bbs=com.edu.bbs.WriteFormImpl
/write.bbs=com.edu.bbs.WriteImpl
/list.bbs=com.edu.bbs.ListImpl
/content.bbs=com.edu.bbs.ContentImpl
/login.bbs=com.edu.bbs.LoginImpl
/logout.bbs=com.edu.bbs.LogoutImpl


  login.jsp를 아래와 같이 작성한다. <input태그의 autofocus 속성은 자동으로 마우스 커서를 해당 <input>태그에 표시해주는 것이고, required 속성은 필수 필드를 지정하여 필요한 내용이 모두 채워졌는 지 확인할 수 있다. required 속성과 같은 경우는 브라우저에서 직접 체크하는 것이므로 오류 메시지 내용은 브라우저들마다 다르게 나타난다(참고1). 이런 속성들을 예전에 자바스크립트나 제이쿼리(jQuery)를 사용해 체크했던 기억이 있는데, 책에서 처음 보고 사용해보니 참 편리하다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>BBS Login</title>
	<link href="bootstrap-3.3.7/css/bootstrap.min.css" rel="stylesheet">
	<style>
		#loginForm {
			width: 20%;
			padding-top: 20%;
			margin: 0 auto;
		}
	
		#login {
			text-align: center;
		}
	</style>
</head>
<body>
	<div id="loginForm">
		<form method="post" action="/bbs/login.bbs">
			<fieldset id="login">
				<legend>BBS</legend>
				<div class="input-group">
					<input type="text" class="form-control" placeholder="Username" aria-describedby="basic-addon1" 
						id="id" name="id" autofocus required minlength="3" maxlength="15">
					<input type="password" class="form-control" placeholder="Password" aria-describedby="basic-addon1"
						id="pw" name="pw" required minlength="3" maxlength="15">
				</div>
				<div class="btn-group" role="group" aria-label="...">
					<input type="submit" class="btn btn-default" value="Login">
					<input type="reset" class="btn btn-default" value="Cancel">
					<input type="button" class="btn btn-default" value="Sign up">
				</div>
			</fieldset>
		</form>
	</div>
</body>
</html>


  USERS 테이블을 생성하고, 회원가입 기능이 없으니 임의의 데이터를 넣어두자.

CREATE TABLE users(
  id VARCHAR2(15) PRIMARY KEY,
  pw VARCHAR2(15) NOT NULL
);

INSERT INTO users VALUES('kim', '1234');

COMMIT;


  LoginStatus인터페이스를 생성하자. 이렇게 특별한 값들을 상수로 선언하여, 클래스에서 상속받아 사용하면 의미가 명확해지고 유용하다.

package com.edu.bbs;

public interface LoginStatus {
	public int LOGIN_SUCCESS = 1;
	public int PASS_FAIL = 2;
	public int NOT_MEMBER = 3;
}

  BBSOracleDao클래스가 LoginStatus인터페이스를 상속받는 것을 확인하기 위해 BBSOracleDao.java파일의 코드를 모두 올렸다. 지금까지 코딩해왔던 메서드들을 확인하고, 마지막 주석이 달려있는 loginCheck메서드를 추가하도록 한다.

package com.edu.bbs;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;

/**
 * BBSOracleDao.java
 * DB를 사용해 데이터를 조회하거나 조작하는 기능을 구현한 클래스
 * 일반, 공지게시판 등 재사용성을 늘리기 위해 클래스로 분리한다.
 */
public class BBSOracleDao implements LoginStatus {
	private static BBSOracleDao bbsOracleDao;
	private OracleDBConnector orclDbc = OracleDBConnector.getInstacne();
	
	Connection conn;
	PreparedStatement pstmt;
	ResultSet rs;
	StringBuffer query;
	
	BBSDto article;
	ArrayList<BBSDto> articles;

	private BBSOracleDao() {}
	
	public static BBSOracleDao getInstance() {
		if(bbsOracleDao == null) {
			bbsOracleDao = new BBSOracleDao();
		}
		return bbsOracleDao;
	}
	
	// synchronized, 한 명의 글쓰기를 처리한 후 다른 사람의 글쓰기를 처리해야한다.
	public synchronized int insertArticle(BBSDto article) throws ClassNotFoundException, SQLException {
		conn = orclDbc.getConnection();
		query = new StringBuffer();
		query.append("INSERT INTO bbs ");
		query.append("VALUES(bbs_seq.nextval, ?, ?, ?, bbs_seq.currval, 0, 0, 0, sysdate, ?)");
		pstmt = conn.prepareStatement(query.toString());
		// parameterIndex는 쿼리문의 ? 순서대로 적어주며, 1부터 시작한다.
		pstmt.setString(1, article.getId());
		pstmt.setString(2, article.getTitle());
		pstmt.setString(3, article.getContent());
		pstmt.setString(4, article.getFileName());
		
		int result = pstmt.executeUpdate();
		
		disconnect();
		
		return result;
	}
	
	public BBSDto selectArticle(String articleNumber) throws ClassNotFoundException, SQLException {
		conn = orclDbc.getConnection();
		query = new StringBuffer();
		query.append("SELECT * FROM bbs WHERE article_number = ?");
		pstmt = conn.prepareStatement(query.toString());
		pstmt.setString(1, articleNumber);
		rs = pstmt.executeQuery();
		
		if(rs.next()) {
			article = new BBSDto();
			article.setArticleNumber(rs.getInt("article_number"));
			article.setId(rs.getString("id"));
			article.setTitle(rs.getString("title"));			
			article.setDepth(rs.getInt("depth"));			
			article.setContent(rs.getString("content"));
			article.setHit(rs.getInt("hit"));
			article.setGroupId(rs.getInt("group_id"));
			article.setPos(rs.getInt("pos"));
			article.setWriteDate(rs.getTimestamp("write_date"));
			article.setFileName(rs.getString("file_name"));
		}
		
		disconnect();
		
		return article;
	}
	
	public ArrayList<BBSDto> selectArticles(int startRow, int endRow) throws ClassNotFoundException, SQLException {
		conn = orclDbc.getConnection();
		query = new StringBuffer();
		// BETWEEN ? AND ? 일 때는 Inline View 두 번 해야한다.
		// 만약 BETWEEN 1 AND ? 이라면 인라인뷰 한 번만으로 가능하다.
		query.append("SELECT bbs.* ");
		query.append("  FROM (SELECT rownum AS row_num, bbs.* ");
		query.append("		    FROM (SELECT article_number, id, title, depth, hit, write_date ");
		query.append("				        FROM bbs ");
		query.append("				       ORDER BY group_id DESC, pos ");
		query.append("				    ) bbs ");
		query.append("		  ) bbs ");
		query.append(" WHERE row_num BETWEEN ? AND ?");
		pstmt = conn.prepareStatement(query.toString());
		pstmt.setInt(1, startRow);
		pstmt.setInt(2, endRow);
		rs = pstmt.executeQuery();
		
		articles = new ArrayList<>();
		
		while(rs.next()) {
			article = new BBSDto();
			article.setArticleNumber(rs.getInt("article_number"));
			article.setId(rs.getString("id"));
			article.setTitle(rs.getString("title"));
			article.setDepth(rs.getInt("depth"));
			article.setHit(rs.getInt("hit"));
			article.setWriteDate(rs.getTimestamp("write_date"));
			articles.add(article);
		}
		
		disconnect();
		
		return articles;
	}
	
	public int getArticleTotalCount() throws ClassNotFoundException, SQLException {
		conn = orclDbc.getConnection();
		pstmt = conn.prepareStatement("SELECT count(*) AS total_count FROM bbs");
		rs = pstmt.executeQuery();
		
		int totalCount = 0;
		
		if(rs.next()) {
			totalCount = rs.getInt(1);		// index로 받아오는 것이 속도면에선 좋다.
	//		totalCount = rs.getInt("total_count");		// 하지만 컬럼명이 보기엔 명확한 듯 하다.
		}
		
		disconnect();
		
		return totalCount;
	}
	
	public synchronized int upHit(String articleNumber) throws ClassNotFoundException, SQLException {
		conn = orclDbc.getConnection();
		pstmt = conn.prepareStatement("UPDATE bbs SET hit = hit + 1 WHERE article_number = ?");
		pstmt.setString(1, articleNumber);
		int result = pstmt.executeUpdate();
		
		disconnect();
		
		return result;
	}

	/**
	 * BBSOracleDao.java에 추가한다.
	 * @param id
	 * @param pw
	 * @return int
	 */
	public int loginCheck(String id, String pw) throws ClassNotFoundException, SQLException {
		conn = orclDbc.getConnection();
		pstmt = conn.prepareStatement("SELECT pw FROM users WHERE id = ?");
		pstmt.setString(1, id);
		rs = pstmt.executeQuery();
		
		int result = 0;
		
		if(rs.next()) {
			if(pw.equals(rs.getString("pw")))
				// 직관적으로 알 수 있도록 상수로 정의하자.
				result = LOGIN_SUCCESS;
			else
				result = PASS_FAIL;
		} else
			result = NOT_MEMBER;
		
		disconnect();
		
		return result;
	}
	
	public void disconnect() throws SQLException {
		if(rs != null) {
			rs.close();
		}
		pstmt.close();
		conn.close();
	}
	
}

  LoginImpl클래스를 생성하고, BBSService, LoginStatus인터페이스를 상속받는다. 그리고 login.jsp에서 입력받은 id를 검증하고 유효하다면 세션에 담고 게시글 리스트 화면을 출력해주고, 유효하지 않다면 다시 로그인 화면을 출력해준다.
package com.edu.bbs;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginImpl implements BBSService, LoginStatus {

	@Override
	public String bbsService(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		
		String id = req.getParameter("id");
		String pw = req.getParameter("pw");
		int result = 0;
		
		try {
			result = BBSOracleDao.getInstance().loginCheck(id, pw);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		if(result == LOGIN_SUCCESS) {
			resp.sendRedirect("/bbs/list.bbs?pageNum=1");

			// session에 id를 심는다.
			req.getSession().setAttribute("id", id);
			return null;
		} else {
			return "login.jsp";
		}
		
	}

}

  loginOk.jsp를 생성하자. 이 화면은 list.jsp 상단 쪽에 출력해줄 화면이다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<link href="bootstrap-3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
	<form action="/bbs/logout.bbs" method="post">
		<span>${id}님, 환영합니다.</span>
		<br>
		<div class="btn-group" role="group" aria-label="...">
			<!-- <button>이 form태그 안에 있으면 type="submit"과 동일 -->
			<button class="btn btn-default btn-xs">Logout</button>
		</div>
	</form>
</body>
</html>


  list.jsp를 조금 수정해야한다. 세션에 id 데이터가 존재한다면 <div id="loginCheckForm">태그 내에 loginOk.jsp화면을 출력해준다. 세션에 id 데이터가 존재하지 않는다면 제이쿼리(jQuery)로 alert창을 띄워준 후 처음 화면으로 돌아가게 한다. 처음에는 브라우저 히스토리(browser history) 객체를 이용해 페이지를 이동시키려고 했으나, 문제가 조금 있는 것 같아 당장은 바로 링크로 넘어가게 해뒀다.

  localhost/bbs/list.bbs&pageNum=1에서 로그아웃하고 localhost/bbs/login.bbs로 돌아갔다고 생각해보자. 브라우저의 '뒤로 가기' 버튼을 클릭하면 다시 /list.bbs&pageNum=1로 넘어간다. 하지만 이때 세션에는 id 데이터가 존재하지 않으므로 글쓰기, 글확인, 페이지 이동을 막아야 한다. 그래서 처음에는 window.history.back()을 사용하려 했다. 하지만 세션에 id 데이터가 없는 상태로 /list.bbs&pageNum=1에서 페이지를 이동하여 /list.bbs&pageNum=3으로 갔다고 한다면, 브라우저 히스토리 객체의 이전 페이지는 /list.bbs&pageNum=1이 된다. 이러한 문제점을 window.history.go()로 조작할 수 있지만, 경우의 수가 있기에 일단은 링크로 처리하게 된 것이다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>BBS List</title>
	<link href="bootstrap-3.3.7/css/bootstrap.min.css" rel="stylesheet">
	<style>
		#listForm {
			width: 70%;
			margin: 0 auto;			/* 가로로 중앙에 배치 */
			padding-top: 5%;			/* 테두리와 내용 사이의 패딩 여백 */
		}
		
		#loginCheckForm {
			text-align: right;
			padding-right: 15px;
			padding-top: 15px;
		}
		
		#listTitle {
			text-align: center;
		}
		
		#writeLink {
			text-align: right;
		}
		
		/* Bootstrap 수정 */
		.table > thead {
			background-color: #e6ecff;
		}
		.table > thead > tr > th {
			text-align: center;
		}
		.table-hover > tbody > tr:hover {
			background-color: #d9def2;
		}
		.table > tbody > tr > td {
			text-align: center;
		}
		.table > tbody > tr > #title {
			text-align: left;
		}
		
		div > #paging {
			text-align: center;
		}
		
		.hit {
			animation-name: blink;
			animation-duration: 1.5s;
			animation-timing-function: ease;
			animation-iteration-count: infinite;
			/* 위 속성들을 한 줄로 표기하기 */
			/* -webkit-animation: blink 1.5s ease infinite; */
		}
		
		/* 애니메이션 지점 설정하기 */
		/* 익스플로러 10 이상, 최신 모던 브라우저에서 지원 */
		@keyframes blink {
			from {color: white;}
			30% {color: yellow;}
			to {color: red; font-weight: bold;}
			/* 0% {color:white;}
			30% {color: yellow;}
			100% {color:red; font-weight: bold;} */
		}
	</style>
</head>
<body>
	<div id="loginCheckForm">
		<c:if test="${id != null}">
			<%@include file="loginOk.jsp" %>
		</c:if>
	</div>
	<div id="listForm">
		<div id="listTitle">
			<b>게시판 글 목록 (전체 글: ${totalCount})</b>
		</div>
		
		<div id="writeLink">
			<a href="/bbs/writeForm.bbs?pageNum=${pageNum}">글쓰기</a>
		</div>
		
		<div>
			<table class="table table-striped table-bordered table-hover">
				<thead>
					<tr>
						<th width="10%">번호</th>
						<th width="50%">제목</th>
						<th width="10%">작성자</th>
						<th width="20%">작성일</th>
						<th width="10%">조회</th>
					</tr>
				</thead>
				<tbody>
					<c:forEach var="article" items="${articles}" varStatus="status">
						<tr>
							<td>${article.articleNumber}</td>
							<td id="title">
								<c:if test="${article.depth > 0}">
									&nbsp;&nbsp;
								</c:if>
								<a href="/bbs/content.bbs?articleNumber=${article.articleNumber}&pageNum=${pageNum}" id="writeLink">${article.title}</a>
								<c:if test="${article.hit >= 20}">
									<span class="hit">hit!</span>
								</c:if>
							</td>
							<td>${article.id}</td>
							<td>${article.writeDate}</td>
							<td>${article.hit}</td>
						<tr>
					</c:forEach>
				</tbody>
			</table>
			<div id="paging">
				${pageCode}
			</div>
		</div>
	</div>
	<script src="https://code.jquery.com/jquery-3.2.0.min.js"></script>
	<script>
		jQuery(document).ready(function() {
			if(${id== null}) {
				alert("게시판을 이용하시려면 로그인하셔야 합니다.");
				location.href="/bbs";
			}
		});
	</script>
</body>
</html>


  jQuery로 id를 검증하는 스크립트를 content.jsp에도 추가해주자. 추가로 디자인도 변경해두었다. 현재 접속해있는(세션에 있는) id와 게시글 작성자(article.id)가 같다면  수정, 삭제 기능이 가능하도록 하였다. 아직 구현하지 않은 기능을 수행하는 '답글달기', '수정하기', '삭제하기' 버튼은 코드가 너무 길어져 주석 처리하지 않았다. 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>BBS Content</title>
	<link href="bootstrap-3.3.7/css/bootstrap.min.css" rel="stylesheet">
	<style>
		#contentForm {
			width: 40%;
			margin: 0 auto;
			padding-top: 5%;
		}

		.table > thead > tr > th, .table > tbody > tr > th {
			background-color: #e6ecff;
			text-align: center;
		}
	</style>
</head>
<body>
	<form action="/bbs/replyForm.bbs" method="post"> 
		<div id="contentForm">
		    <input type="hidden" name="pageNum" value="${pageNum}">
		    <input type="hidden" name="depth" value="${article.depth}">
		    <input type="hidden" name="pos" value="${article.pos}">
		    <input type="hidden" name="groupId" value="${article.groupId}">
		    
		    <div>
			    <table class="table table-striped table-bordered">
					<thead>
						<tr>
							<th>글쓴이</th>
							<td>${article.id}</td>
							<th>조회수</th>
							<td>${article.hit}</td>
						</tr>
						<tr>
							<th>제목</th>
							<td>${article.title}</td>
							<th>날짜</th>
							<td>${article.writeDate}</td>
						</tr>
						<tr>
							<th colspan="2">다운로드</th>
							<td colspan="2">
								<!-- 파일 다운로드 기능 -->
								<a href="">${article.fileName}</a>
								<%-- <a href="/bbs/download.bbs?fileName=${article.fileName}">${article.fileName}</a> --%>
							</td>
						</tr>
					</thead>
					<tbody>
						<tr height="200" valign="top">
							<td colspan="4">${article.content}</td>
						</tr>
						<tr>
							<th>첨부</th>
							<td colspan="3">
								<c:if test="${article.fileName == null}">없음</c:if>
								<c:if test="${article.fileName != null}">${article.fileName}</c:if>
							</td>
						</tr>
					</tbody>
				</table>
				
		    	<div id="btns" class="btn-group btn-group-sm" role="group" aria-label="...">
					<c:if test="${id != null}">
						<input type="submit" class="btn btn-default" value="답글달기">
						<c:if test="${id == article.id}">
							<input type="button" class="btn btn-default" value="수정하기" onclick="document.location.href='/bbs/updateForm.bbs?articleNumber=${article.articleNumber}&pageNum=${pageNum}'">
							<input type="button" class="btn btn-default" value="삭제하기" onclick="document.location.href='/bbs/delete.bbs?articleNumber=${article.articleNumber}&pageNum=${pageNum}'">
						</c:if>
						<c:if test="${id != article.id}">
							<input type="button" class="btn btn-default" value="수정하기" disabled="disabled">
							<input type="button" class="btn btn-default" value="삭제하기" disabled="disabled">
						</c:if>
					</c:if>
					<c:if test="${id == null}">
						<input type="submit" class="btn btn-default" value="답글달기" disabled="disabled">
						<input type="button" class="btn btn-default" value="수정하기" disabled="disabled">
						<input type="button" class="btn btn-default" value="삭제하기" disabled="disabled">
					</c:if>
					<input type="button" class="btn btn-default" value="목록으로" onclick="document.location.href='/bbs/list.bbs?pageNum=${pageNum}'">
				</div>
		    </div>
		</div>
	</form>

	<script src="https://code.jquery.com/jquery-3.2.0.min.js"></script>
	<script>
		jQuery(document).ready(function() {
			if(${id== null}) {
				alert("게시판을 이용하시려면 로그인하셔야 합니다.");
				location.href="/bbs/login.bbs";
			}
		});
	</script>
</body>
</html>


  Logout클래스를 생성한다. 현재 세션의 모든 속성을 삭제한 후 페이지를 이동시킨다.

package com.edu.bbs;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LogoutImpl implements BBSService {

	@Override
	public String bbsService(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		// 세션의 모든 속성을 삭제한다.
		req.getSession().invalidate();

		resp.sendRedirect("/bbs/login.bbs");
		
		return null;
	}

}

  브라우저로 어떻게 동작하는 지 확인해보자. 아래는 http://localhost/bbs로 접근하여 로그인하는 화면이다.


  아래는 로그아웃한 후 세션에 id 데이터가 없기 때문에 더이상 로그인이 필요함을 알려주는 화면이다.


  다음 글에서는 글을 갱싱(update), 삭제(delete) 기능을 구현할 계획이다.



[「Do it! HTML5+CSS3 웹 표준의 정석」 교재 참고1]

[Session이란? 참고]

[[JSP]웹에서 세션(session)의 사용 참고]

[Javascript 페이지 이동 참고]

[[HTML5] 브라우저 히스토리 다루기(browser history) 참고]