본문 바로가기

개발관련/코드로 배우는 스프링 웹 프로젝트(개정판)

Part3 - 페이징 화면 처리 Chapter14

URL의 파라미터를 이용해서 정상적으로 원하는 페이지로 이동한느 것을 확인했다면, 화면 밑에 페이지 번호를 표시하고 사용자가 페이지 번호를 클릭할수 있게 처리합니다.

페이지를 보여주는 작업은 다음과 같은 과정을 통해서 진행 합니다.

  • 브라우저 주소창에서 페이지 번호를 전달해서 결과를 확인하는 단계
  • JSP에서 페이지 번호를 출력하는 단계
  • 각 페이지 번호에 클릭 이벤트 처리
  • 전체 데이터 개수를 반영해서 페이지 번호 조절

페이지 처리는 단순히 링크의 연결이기 때문에 어렵지는 않치만 다음 그림과 같이 목록 페이지에서 조회 페이지, 수정 삭제 페이지 까지 신경 써야 하는 부분이 많은 편입니다.

다음 그림은 페이지 번호가 어떤 작업을 하던 유지되면서 링크가 연결되는 모습 입니다.

14.1 페이징 처리할 때 필요한 정보들

화면에 페이징 처리를 하기 위해서는 우선적으로 여러가지 필요한 정보들이 존재 합니다.

화면에 페이지는 크게 다음과 같은 정보들이 필요합니다.

  • 현재 페이지 번호(page)
  • 이전과 다음으로 이동 가능한 링크의 표시 여부(prev, next)
  • 화면에서 보여지는 페이지의 시작 번호와 끝 번호(startPage,endPage)

14.1.1 끝 페이지 번호와 시작 페이지 번호

페이징 처리를 하기 위해서 우선적으로 필요한 정보는 현재 사용자가 보고 있는 페이지(page)의 정보 입니다.

예를 들어, 사용자가 5페이지를 본다면 화면의 페이지 번호는 1부터 시작하지만, 사용자가 19페이지를 본다면 11부터 시작해야 하기 때문입니다.

(화면에 10개씩 출력 한다고 가정)

 

흔히들 페이지를 계산할때 시작 번호를 먼저 하려고 하지만, 오히려 끝 번호를 먼저 계산 해 두는 것이 수월 합니다.

끝 번호는 다음과 같은 공식으로 구할 수 있습니다.

(페이지 번호가 10개씩 보인다고 가정하겠습니다.)

 

this.endPage = (int)(Math.ceil(페이지번호 / 10.0)) * 10;

Math.ceil() 함수는 소수점을 올림으로 처리하기 때문에 다음과 같은 상황이 가능 합니다.

  • 1페이지의 경우 : Math.ceil(0.1) * 10 = 10
  • 10페이지의 경우 : Math.ceil(1) * 10 = 10
  • 11페이지의 경우 Math.ceil(1.1) * 10 = 20

끝 번호(endPage)는 아직 개선의 여지가 있습니다.

만일 전체 데이터 수가 적다면 10페이지로 끝나면 안되는 상황이 생길수도 있기 때문입니다.

그럼에도 끝 번호(endPage)를 먼저 계산하는 이유는 시작번호(startPage)를 계산하기 수월하기 때문입니다.

 

만일 화면에 10개씩 보여준다면 시작번호(startPage)는 무조건 끝 번호(endPage)에서 9라는 값을 뺀 값이 됩니다.

this.startPage = this.endPage - 9;

끝 번호(endPage)는 전체 데이터 수(total)에 의해서 영향을 받습니다.

예를 들어, 10개씩 보여주는 경우 전체 데이터 수(total)이 80개라고 가정하면 끝 번호(endPage)는 10이 아닌 8이 되어야만합니다

 

만일 끝 번호(endPage)와 한 페이지당 출력되는 데이터수(amount)의 곱이 전체 데이터 수(total)보다 크다면 끝 번호(endPage)는 다시 total을 이용해서 다시 계산 되어야 합니다.

realEnd = (int)(Math.ceil((total * 1.0) / amount) );

if(realEnd < this.endPage) {
	this.endPage = realEnd;
}

먼저 전체 데이터수(total)을 이용해서 진짜 끝 페이지(realEnd)가 몇 번까지 되는지를 계산 합니다.

만일 진짜 끝 페이지(realEnd)가 구해둔 끝 번호(endPage)보다 작다면 끝번호는 작은 값이 되어야만 합니다.

 

 

이전(prev)와 다음(next)

이전(prev)과 다음(next)은 아주 간단히 구할 수 있습니다.

이전(prev)의 경우는 시작 번호(startPage)가 1보다 큰 경우라면 존재하게 됩니다.

//이전(prev)계산
this.prev = this.startPage > 1;

다음(next)으로 가능 링크의 경우 위의 realEnd가 끝 번호(endPage)보다 큰 경우에만 존재하게 됩니다.

//다음(next) 계산
this.next = this.endPage < realEnd;

 

14.2 페이징 처리를 위한 클래스 설계

화면에 페이징 처리를 위해서 위와 같이 여러 정보가 필요하다면 클래스를 구성해서 처리하는 방식도 편한 방식이 될 수 있습니다.

클래스를 구성하면 Controller 계층에서 JSP 화면에 전달할 때에도 객체를 생성해서 Model에 담아 보내는 과정이 단순해지는 장점도 있습니다.

 

org.zerock.domain 패키지에 PageDTO 클래스를 설계 합니다.

package org.zerock.domain;

import lombok.Getter;
import lombok.ToString;

@Getter
@ToString
public class PageDTO {
	private int startpage;
	private int endPage;
	private boolean prev, next;
	
	private int total;
	private Criteria cri;
	
	public PageDTO(Criteria cri,int total ) {
		this.cri = cri;
		this.total = total;
		
		this.endPage = (int)(Math.ceil(cri.getPageNum() / 10.0)) * 10;
		this.startpage = this.endPage - 9;
		
		int realEnd = (int)(Math.ceil((total * 1.0) / cri.getAmount()));
		
		if(realEnd < this.endPage) {
			this.endPage = realEnd;
		}
		
		this.prev = this.startpage > 1;
		
		this.next = this.endPage < realEnd;
	}
	
}

PageDTO는 생성자를 정의하고 Criteria와 전체 데이터수(total)를 파라미터로 지정합니다.

Criteria 안에는 페이지에서 보여주는 데이터수(amount)와 현재 페이지 번호(pageNum)를 가지고 있기 때문에 이를 이용해서 필요한 모든 내용을 계산할 수 있습니다.

 

BoardController에서는 PageDTO를 사용할 수 있도록 Model에 담아서 화면에 전달 해 줄 필요가 있습니다.

아래와 같이 수정 합니다.

	@GetMapping("/list")
	public void list(Criteria cri, Model model) {
		log.info("list"+cri);
		model.addAttribute("list",service.getList(cri));
		model.addAttribute("pageMaker",new PageDTO(cri,123)); //추가
	}

list()는 "pageMaker"라는 이름으로 PageDTO 클래스에 객체를 만들어서 Model에 담아줍니다.

PageDTO를 구성하기 위해서는 전체 데이터 수가 필요한데 아직 그 처리가 이루어 지지 않았으므로 임의이 값으로 "123"을 넣었습니다.

 

14.3 JSP에서 페이지 번호 출력

JSP에서 페이지 번호를 출력하는 부분은 JSTL을 이용해서 처리 할 수 있습니다.

SBAdmin2는 부트 스트랩 기반으로 구성 되어 있기 때문에 

 

예제는 SBAdmin2의 pages 폴더에 있는 tables.html 페이지의 페이지 처리를 이용해서 구성 합니다.

기존 <table> 태그가 끝나는 직후에 페이지 처리를 추가 합니다.

 

Modal 창의 아래쪽에 별도의 <div class="row">를 구성하고 페이지 번호들을 출력 합니다.

pageMaker라는 이름으로 전달된 PageDTO를 이용해서 화면에 페이지 번호들을 출력 합니다.

 

예를 들어 현재 total은 123이라는 숫자로 지정되어 있으므로 5페이지를 조회하면 next값은 true가 되어야 합니다.

반면에 amount 값이 20인 경우에는 7페이지 까지만 출력되어야 합니다.

<div class='pull-right'>
    <ul class="pagination">

    <c:if test="${pageMaker.prev}">
      <li class="paginate_button previous"><a href="#">Previous</a>
      </li>
    </c:if>

    <c:forEach var="num" begin="${pageMaker.startPage}"
      end="${pageMaker.endPage}">
      <li class="paginate_button"><a href="#">${num}</a></li>
    </c:forEach>

    <c:if test="${pageMaker.next}">
      <li class="paginate_button next"><a href="#">Next</a></li>
    </c:if> 

    </ul>
</div>
<!--  end Pagination -->

 

14.3.1 페이지 번호 이벤트 처리

화면에서 페이지 번호가 보이기는 하지만 아직 페이지 번호를 클릭 했을때 이벤트 처리가 되지 않았습니다.

일반적으로 <a> 태그의 href 속성을 이용하는 방법을 사용 할 수 도 있지만, 직접 링크를 처리하는 방식의 경우 검색 조건이 붙고 난 후에 처리가 복잡하게 되므로 JavaScript를 통해 처리하겠습니다.

 

우선 페이지와 관련된 <a>태그의 href 속성값으로 페이지 번호를 가지도록 수정 합니다.

(번호 출력은 <c:out>을 사용 하는 것이 좋치만 예제에서는 가독성의 문제로 일반 EL을 사용 했습니다)

<!-- table태그의 끝 -->

<div class='pull-right'>
    <ul class="pagination">

    <c:if test="${pageMaker.prev}">
      <li class="paginate_button previous"><a href="${pageMaker.startPage -1}">Previous</a>
      </li>
    </c:if>

    <c:forEach var="num" begin="${pageMaker.startPage}"
      end="${pageMaker.endPage}">
      <li class="paginate_button ${pageMaker.cri.pageNum == num ? 'active': ''}"><a href="${num}">${num}</a></li>
    </c:forEach>

    <c:if test="${pageMaker.next}">
      <li class="paginate_button next"><a href="${pageMaker.endPage +1}">Next</a></li>
    </c:if> 

    </ul>
</div>
<!--  end Pagination -->

화면에서는 <a>태그는 href 속성값으로 단순히 번호만을 가지게 변경 합니다.

브라우저에서 만들어진 결과를 보면 아래와 같습니다.

이 상태에서 페이지 번호를 클릭하게 되면 해당하는 URL이 존재하지 않기 때문에 아래와 같은 메세지가 나오게 됩니다.

 

<a>태그가 원래의 동작을 못하도록 JavaScript 처리를 하겠습니다.

실제 페이지를 클릭하면 동작을 한느 부분은 별도의 <form> 태그를 이용해서 처리하도록 합니다.

<c:out>을 사용하는것이 더 좋은 방법이지만 간단히 처리 하기 위해 EL을 사용 하겠습니다.

<form id="actionForm" action="/board/list" method="get">
    <input type="hidden" name="pageNum" value="${pageMaker.cri.pageNum}" >
    <input type="hidden" name="amount" value="${pageMaker.cri.amount}" >
</form>
<!--  end Pagination -->
	<script type="text/javascript">
		$(document).ready(function(){
			var result = '<c:out value="${result}"/>';
			
			checkModal(result);
			
			history.replaceState({}, null, null);
			
			
			function checkModal(result) {
				if (result === '' || history.state) {
					return;
				}

				if (parseInt(result) > 0 || history.state) {
					$(".modal-body").html("게시글 " + parseInt(result) + " 번이 등록되었습니다.");
				}
				$("#myModal").modal("show");
			}
			
			$("#regBtn").on("click", function(){
				self.location="/board/register";
			});
			
			var actionForm = $("#actionForm");

			$(".paginate_button a").on("click", function(e) {

						e.preventDefault();

						console.log('click');

						actionForm.find("input[name='pageNum']").val($(this).attr("href"));
						actionForm.submit();
			});
			
		});
		
	</script>

list.jsp에서는 <form> 태그를 이용해서 URL을 이동 합니다.

JavaScript에서는 <a> 태그를 클릭해도 페이지 이동이 없도록 preventDefault() 처리를 했습니다.

<form> 태그 내 pageNum 값은 href 속성값으로 변경 합니다.

 

이 처리를 하고 나면 화면에서 페이지 번호를 클릭 했을 때 <form> 태그 내의 페이지 번호가 변경 되는 것을 확인 할 수 있습니다.

 

마지막 처리는 actionForm을 submit() 해줘야 합니다.

 

브라우저에서 페이지 번호를 클릭하면 화면에서 제대로 이동 하는지 확인 합니다.

14.4 조회 페이지로 이동

목록 화면에서 페이지 번호를 클릭하면 정상적으로 원하는 페이지로 이동하는 것을 볼수 있지만 아직 몇 가지 문제가 있습니다.

 

사용자가 3페이지에 있는 게시물을 클릭한 후 다시 목록으로 이동해보면 다음과 같이 무조건 1페이지 목록으로 이동합니다.

 

List 버튼을 클릭했을때 pageNum과 amount를 아직 주지 않아 400 에러가 떨어지는 것을 확인 할 수 있습니다.

 

 

지금은 페이징 처리를 하고 나면 특정 게시물의 조회 페이지로 이동 한 후 다시 목록으로 돌아가는데 문제가 생깁니다.

조회 페이지에서 List 버튼을 클릭하면 다시 1페이지로 돌아가게 되는데 이를 해결하기 위해서는 조회 페이지로 갈때 목록 페이지의 pageNum과 amount를 같이 전달 해야 합니다.

 

이런 경우 페이지 이동에 사용했던 <form> 태그에 추가로 게시물의 번호를 같이 전송하고, action 값을 조정해서 처리 할 수 있습니다.

 

원래 게시물의 제목에는 "/board/get?bno=xxx"로 이동 할 수 있는 링크가 직접 처리 되어 있습니다.

list.jsp

 

페이지 번호는 조회 페이지에 전달되지 않기 때문에 조회 페이지에서 목록 페이지로 이동 할 때는 아무런 정보가 없이 다시 "/board/list"를 호출하게 됩니다. 

 

간단하게는 각 게시물의 링크에 추가로 "&pageNum=xx"와 같이 처리할 수도 있지만 나중에 여러 조건들이 추가되는 상황에서는 복잡한 링크를 생성해야만 합니다.

 

<a>태그로 복잡한 링크를 생성하는 방식이 나쁘다고 말할 수 없습니다.

가장 대표적인 예가 검색엔진 입니다.

검색엔진에서는 출력된 정보와 링크를 저장해서 사용하기 때문에 <a> 태그 내의 링크가 완전한 URL인 경우가 노출에 유리합니다.

 

만일 단일 웹페이지가 검색엔진에 의해 노출이 피룡한 경우라면 직접 모든 문자열을 구성해 주는 방식이 더 좋습니다.

 

직접 링크로 연결된 경로를 페이지 이동과 마찬가지로 <form> 태그를 이용해서 처리할 것이므로 <a> 태그에는 이동하려는 게시물의 번호만을 가지게 수정 합니다.

(이벤트 처리를 수월하게 하기 위해 <a> 태그에 class 속성을 하나 부여 했습니다)

<c:forEach items="${list}" var="board">
    <tr>
        <td><c:out value="${board.bno}" /></td>
        <td>
            <a class="move" href='<c:out value="${board.bno}" />'>
            <c:out value="${board.title}" /> </a>
        </td>
        <td><c:out value="${board.writer}" /></td>
        <td><fmt:formatDate pattern="yyyy-MM-dd" value="${board.regdate}" /></td>
        <td><fmt:formatDate pattern="yyyy-MM-dd" value="${board.updateDate}" /></td>
    </tr>
</c:forEach>

 

화면에서는 조회 페이지로 가능 링크 대신에 단순히 번호만이 출력 됩니다.

(마우스를 올려보면 아래 쪽에서 확인이 가능 합니다)

실제 클릭은 JavaScript를 이용해서 게시물의 제목을 클릭 했을때 이동하도록 이벤트 처리를 해보겠습니다.

$(".move").on("click",function(e) {
    e.preventDefault();
    actionForm.append("<input type='hidden' name='bno' value='"	+ $(this).attr("href")+ "'>");
    actionForm.attr("action","/board/get");
    actionForm.submit();
});

게시물의 제목을 클릭하면 <form> 태그에 추가로 bno값을 전송하기 위해서 <input> 태그를 만들어 추가하고,

<form> 태그의 action은 "/board/get"으로 변경 합니다.

위의 처리가 정상적으로 완료 되었다면 게시물의 제목을 클릭 했을 때 pageNum과 amount 파라미터가 전달 되는 것을 볼 수 있습니다.

 

14.4.1 조회 페이지에서 다시 목록페이지로 이동 - 페이지 번호 유지

조회 페이지에 다시 목록 페이지로 이동하기 위한 파라미터들이 같이 전송 되었다면 조회 페이지에서 목록 페이지로 이동하기 위한 이벤트 처리를 해야 합니다.

BoardController의 get() 메서드는 원래 게시물의 번호만 받도록 처리 되어 있지만, 추가적인 파라미터가 붙으면서 Criteria를 파라미터로 추가해서 받고 전달 합니다.

 

@ModelAttribute는 자동으로 Model에 데이터를지정한 이름으로 담아 줍니다.

@ModelAttribute를 사용하지 않아도 Controller에서 화면으로 파라미터가 된 객체는 전달이 되지만, 좀 더 명시적으로 이름을 지정하기 위해 사용 합니다.

 

기존 get.jsp에서는 버튼을 클릭하면 <form> 태그를 이용하는 방식이였으므로 필요한 데이터를 추가해서 이동 하도록 수정 합니다.

<form id='operForm' action="/boad/modify" method="get">
  <input type='hidden' id='bno' name='bno' value='<c:out value="${board.bno}"/>'>
  <input type='hidden' name='pageNum' value='<c:out value="${cri.pageNum}"/>'>
  <input type='hidden' name='amount' value='<c:out value="${cri.amount}"/>'>
</form>

get.jsp는 operForm이라는 id를 가진 <form> 태그를 이미 이용했기 때문에 cri라는 이름으로 전달된 Criteria 개겣를 이용해서 pageNum과 amount 값을 태그로 구성하고, 버튼을 클릭 했을 때 정상적으로 목록 페이지로 이동하게 처리 합니다.

 

14.4.2 조회 페이지에서 수정/삭제 페이지로 이동

조회 페이지에서는 "Modify" 버튼을 통해서 수정/삭제 페이지로 이동하게 됩니다.

수정/학제 페이지에서는 다시 목록으로 가는 버튼이 존재 하므로 동일하게 목록 페이지에 필요한 파라미터들을 처리해야 합니다.

BoardController에서는 get() 메서드에 "/get"과 "/modify"를 같이 처리하므로 별도의 추가적인 처리 없이도 Criteria를 Model에 cri라는 이름으로 담아서 전달 합니다.

 

조회 페이지에서 <form> 태그는 목록 페이지로의 이동뿐 아니라 수정/삭제 페이지 이동에도 사용되기 때문에 파라미터들은 자동으로 값이 전송 됩니다.

 

14.5 수정과 삭제 처리

modify.jsp에서는 <form> 태그를 이용해서 데이터를 처리합니다.

거의 입력과 비슷한 방식으로 구현 되는데, 이제 pageNum과 amount라는 값이 존재하므로 <form> 태그내에서 같이 전송할 수 있게 수정 해야 합니다.

<input type='hidden' name='pageNum' value='<c:out value="${cri.pageNum }"/>'>
<input type='hidden' name='amount' value='<c:out value="${cri.amount }"/>'>

modify.jsp 역시 Criteria를 Model에서 사용하기 때문에 위와 같이 태그를 만들어서 <form> 태그 전송에 포함 합니다.

 

14.5.1 수정/삭제 처리 후 이동

POST방식으로 진행하는 수정과 삭제 처리는 BoardController에서 각각의 메서드 형태로 구현되어 있으므로 페이지 관련 파라미터들을 처리하기 위해서는 변경해 줄 필요가 있습니다.

	@PostMapping("/modify")
	public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
		log.info("modify" + board);
		
		if(service.modify(board)) {
			rttr.addFlashAttribute("result","success");
		}
		
		rttr.addAttribute("pageNum",cri.getPageNum());
		rttr.addAttribute("amount",cri.getAmount());
		
		return "redirect:/board/list";
	}

메서드의 파라미터에는 Criteria가 추가된 형태로 변경되고,

RedirectAttributes 역시 URL 뒤에 원래의 페이지로 이동하기 위해서 pageNum과 amount값을 가지고 이동하게 수정 합니다.

 

삭제 처리 동일하게 Criteria를 받아들이는 형식으로 변경 합니다.

	@PostMapping("/remove")
	public String remove(@RequestParam("bno") Long bno, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
		log.info("remove....."+bno);
		
		if(service.remove(bno)) {
			rttr.addFlashAttribute("result","success");
		}
		
		rttr.addAttribute("pageNum",cri.getPageNum());
		rttr.addAttribute("amount",cri.getAmount());
		
		return "redirect:/board/list";
				
	}

위와 같은 방식을 이용하면 수정/삭제 후 기존 사용자가 보던 페이지로 이동 하는 것을 확인 할 수 있습니다.

수정과 달리 삭제는 처리 후 1페이지로 이동해도 무방하지만 이왕이면 사용자들에게 자신이 보던 정보를 이어서 볼 수 있게 조치해주는 방식 역시 어렵지 않습니다.

 

14.5.2 수정/삭제 페이지에서 목록 페이지로 이동

페이지 이동은 마지막은 수정/삭제를 취소하고 다시 목록페이지로 이동하는 것입니다.

목록 페이지는 오직 pageNum과 amount만을 사용하므로 <form> 태그의 다른 내용들은 삭제하고 필요한 내용만을 다시 추가하는 형태가 됩니다.

<script type="text/javascript">
$(document).ready(function() {

	  var formObj = $("form");

	  $('button').on("click", function(e){
	    
	    e.preventDefault(); 
	    
	    var operation = $(this).data("oper");
	    
	    console.log(operation);
	    
	    if(operation === 'remove'){
	      formObj.attr("action", "/board/remove");
	      
	    }else if(operation === 'list'){
	      //move to list
	      formObj.attr("action", "/board/list").attr("method","get");
	      
	      var pageNumTag = $("input[name='pageNum']").clone();
	      var amountTag = $("input[name='amount']").clone();
	      
	      formObj.empty();
	      
	      formObj.append(pageNumTag);
	      formObj.append(amountTag);
      
	    }
	    
	    formObj.submit();
	  });

});
</script>

만일 사용자가 "List" 버튼을 클릭한다면 <form>  태그에서 필요한 부분만 잠시 복사(clone) 해서 보관해 두고, <form> 태그 내의 모든 내용은 지워버립니다(empty), 이후에 다시 필요한 태그들만 추가해서 "/board/list"를 호출하는 형태를 이용합니다.

 

14.6 MyBatis 에서 전체 데이터의 개수 처리

페이지의 이동이 모든 작업에서 정상적으로 이뤄지는 것을 확인 했다면 최종적으로는 데이터베이스에 있는 실제 모든 게시물 수(total)를 구해서 PageDTO를 구성할 때 전달해 줘야 합니다.

 

전체의 개수를 구하는 SQL은 복잡하지 않기 때문에 어노테이션으로 처리해도 무방 하지만 BoardMapper 인터페이스에 getTotalCount() 메서드를 정의하고 XML를 이용해보겠습니다.

 

public int getTotalCount(Criteria cri);

getTotalCount()는 Criteria를 파라미터로 전달받도록 설계하지 않아도 문제가 생기지는 않치만 게시물의 목록과 전체 데이터 수를 구하는 작업은 일관성 있게 Criteria를 받는것이 좋습니다.(잠시 후 검색에 필요 합니다.)

BoardService와 BoardServiceImpl에서는 별도의 메서드를 작성해서 BoardMapper의 getTotalCount()을 호출 합니다.

BoardService.java

public int getTotalCount(Criteria cri);

 

BoardServiceImpl.java

	@Override
	public int getTotalCount(Criteria cri) {
		return mapper.getTotalCount(cri);
	}

BoardController에서는 BoardService 인터페이스를 통해서 getTotalCount()을 호출하도록 변경 합니다.

	@GetMapping("/list")
	public void list(Criteria cri, Model model) {
		log.info("list"+cri);
		model.addAttribute("list",service.getList(cri));
		
		int total = service.getTotalCount(cri);
		
		log.info("total : " + total);
		
		model.addAttribute("pageMaker",new PageDTO(cri,total));
	}

 

이상으로 게시물의 등록, 수정, 삭제, 조회, 페이징 처리가 완료 되었습니다.

남은 작업은 검색 조건을 이용하는 처리입니다.

 

다음 장을 학습하기 전에 지금까지 학습한 내용이 어렵다면 다시한번 복습 해보시면 좋을것 같습니다.