HTML에서 자바스크립트 분리

이벤트 핸들러를 지정하는 데는 두 가지 방식이 있다.

1. HTML 엘리먼트 속성에 바로 지정하는 방식

<input type="button" value="버튼" onclick="이벤트 핸들러" />

HTML 속성에 이벤트 핸들러를 지정하면 HTML과 자바스크립트의 결합도가 높아진다.
DOM 모델을 활용해서 이벤트 핸들러를 지정할 수 있는데, 이렇게 하면 HTML과 자바스크립트의 결합도를 조금 낮출 수 있다.
지정을 보다 편하게 하기 위해 HTML 엘리먼트에 id 속성을 추가한다.

<input type="button" id="some-button" value="버튼" />

2. DOM 모델을 활용해 지정하는 방식

window.onload = function() {
	var btn = document.getElementById("some-button");
	btn.onclick = function() {
		//TODO
	};
};

웹 브라우저는 HTML 문서를 해석하여 보여준다.
정확하게 말하면, HTML 문서로부터 DOM 트리를 생성한 후 DOM 트리를 해석해서 보여준다.
모든 문서가 준비되었을 때 발생하는 이벤트가 window의 onload 이벤트다.
window의 onload 이벤트에 이벤트 핸들러가 지정하는 방식에도 단점이 있다.
필요하지 않았던 글로벌 속성(id,class,title)을 추가해야 하고, 문서에 이미지가 많을수록 핸들러 등록이 늦어진다. (window의 onload 이벤트는 이미지가 모두 로드된 후 발생하기 때문이며, 절 후반에 제이쿼리를 사용하면 개선할 수 있다)
이 절에서 우리는 HTML 속성에 지정한 핸들러를 제거하고, DOM을 활용해서 핸들러를 지정하는 실습을 할 것이다. 실습 대상은 게시판의 view.jsp 파일이다.

첨부 파일 링크와 첨부 파일 삭제 링크

첨부 파일링크와 첨부 파일 삭제 링크

다운로드 링크와 삭제 링크를 다음과 같이 수정한다.

<div id="detail">
	<div id="date-writer-hit">
		edited <fmt:formatDate pattern="yyyy.MM.dd HH:mm:ss" value="${regdate }" /> by ${name } hit {hit }
	</div>
	<div id="article-content">${content }</div>
	<div id="file-list" style="text-align: right;">
		<c:forEach var="file" items="${attachFileList }" varStatus="status">
			<div class="attach-file">
			<a href="#" title="${file.filename }" class="download">${file.filename }</a>
			<security:authorize access="#email == principal.username or hasRole('ROLE_ADMIN')">
				<a href="#" title="${file.attachFileNo }">삭제</a>
			</security:authorize>
		</div>
		</c:forEach>
	</div>        
</div>

<script>와 </script> 사이에 다음을 추가한다.

window.onload = initPage;

function initPage() {
  //첨부 파일 다운로드, 첨부 파일 삭제
  var file_list = document.getElementById("file-list");
  var fileLinks = file_list.getElementsByTagName("a");
  
  for (var i = 0; i < fileLinks.length; i++) {
    var fileLink = fileLinks[i];
    if (fileLink.className == "download") {
      fileLink.onclick = function() {
        var attachFileNo = this.title;
        var form = document.getElementById("downForm");
        form.attachFileNo.value = attachFileNo;
        form.submit();
        return false;
      };
    } else {
      fileLink.onclick = function() {
        var attachFileNo = this.title;
        var chk = confirm("정말로 삭제하겠습니까?");
        if (chk === true) {
          var form = document.getElementById("deleteAttachFileForm");
          form.attachFileNo.value = attachFileNo;
          form.submit();
          return false;
        }
      };
    }
  }
  //TODO 진행하면서 이곳에 코드를 추가한다.
  
}//initPage 함수 끝

댓글

댓글 반복
관련 코드를 다음처럼 수정한다.

<div id="all-comments">
  <c:forEach var="comment" items="${commentList }" varStatus="status">
  <div class="comments">
    <span class="writer">${comment.name }</span>
    <span class="date">${comment.regdate }</span>
    <security:authorize access="#comment.email == principal.username or hasRole('ROLE_ADMIN')">
    <span class="modify-del">
      <a href="#">수정</a> | <a href="#" title="${comment.commentNo }">삭제</a>
    </span>
    </security:authorize>
    <div class="comment-memo">${comment.memo }</div>
    <form class="comment-form" action="updateComment" method="post" style="display: none">
      <input type="hidden" name="commentNo" value="${comment.commentNo }" />
      <input type="hidden" name="boardCd" value="${param.boardCd }" />
      <input type="hidden" name="articleNo" value="${param.articleNo }" />
      <input type="hidden" name="page" value="${param.page }" />
      <input type="hidden" name="searchWord" value="${param.searchWord }" />
      <div style="text-align: right;">
        <a href="#">전송</a> | <a href="#">취소</a>
      </div>
      <div>
        <textarea class="comment-textarea" name="memo" rows="7" cols="50">${comment.memo }</textarea>
      </div>
    </form>
  </div>
  </c:forEach>
</div>

initPage() 함수 밖에 다음을 추가한다.

function commentUpdate(e) {
  var me = getActivatedObject(e);
  var form = me.parentNode;
  while (form.className != "comment-form") {
    form = form.parentNode;
  }
  form.submit();
  return false;
}
function modifyCommentToggle(e) {
  var me = getActivatedObject(e);
  var comments = me.parentNode;
  while (comments.className != "comments") {
    comments = comments.parentNode;
  }
  var div = comments.getElementsByTagName("div")[0];//댓글 내용
  var form = comments.getElementsByTagName("form")[0];//댓글 form
  if (div.style.display) {
    div.style.display = '';
    form.style.display = 'none';
  } else {
    div.style.display = 'none';
    form.style.display = '';
  }
  return false; 
}
/*
 Head First Ajax 참조 
*/
function getActivatedObject(e) {
  var obj;
  if (!e) {
    //IE 옛 버전
    obj = window.event.srcElement;
  } else if (e.srcElement) {
    //IE 7 이상
    obj = e.srcElement;
  } else {
    //DOM 레벨 2 브라우저
    obj = e.target;
  }
  return obj;
}

initPage() 함수에 다음 코드를 추가한다.

//댓글
var allComments = document.getElementById("all-comments");
var divs = allComments.getElementsByTagName("div");

for (i = 0; i < divs.length; i++) {
  if (divs[i].className == "comments") {
    var comments = divs[i];
    var spans = comments.getElementsByTagName("span");
    for (var j = 0; j < spans.length; j++) {
      if (spans[j].className === "modify-del") {
        var md = spans[j];
        var commentModifyLink = md.getElementsByTagName("a")[0];//수정 링크
        commentModifyLink.onclick = modifyCommentToggle;
        var commentDelLink = md.getElementsByTagName("a")[1];//삭제 링크
        commentDelLink.onclick = function() {
          var commentNo = this.title;
          var chk = confirm("정말로 삭제하겠습니까?");
          if (chk === true) {
            var form = document.getElementById("deleteCommentForm");
            form.commentNo.value = commentNo;
            form.submit();
            return false;
           }
         };
      }
      //form 태그 안의 수정하기 링크
      var form = comments.getElementsByTagName("form")[0];
      var div = form.getElementsByTagName("div")[0];
      commentModifyLink = div.getElementsByTagName("a")[0];
      commentModifyLink.onclick = commentUpdate;
      //form 태그 안의 취소링크
      var cancelLink = div.getElementsByTagName("a")[1];
      cancelLink.onclick = modifyCommentToggle;
    }
  }  
}

이전 글 링크와 다음 글 링크

다음 글 이전 글 링크
관련 코드를 수정한다.

<div id="next-prev">
  <c:if test="${nextArticle != null }">
    <p>다음 글 : <a href="#" title="${nextArticle.articleNo }">${nextArticle.title } </a></p>
  </c:if>
  <c:if test="${prevArticle != null }">
    <p>이전 글 : <a href="#" title="${prevArticle.articleNo }">${prevArticle.article.title }</a></p>
  </c:if>
</div>

initPage() 함수에 다음 코드를 추가한다.

//다음 글, 이전 글 링크
var nextPrev = document.getElementById("next-prev");
links = nextPrev.getElementsByTagName("a");
for (i = 0; i < links.length; i++) {
  links[i].onclick = function() {
    var form = document.getElementById("viewForm");
    form.articleNo.value = this.title;
    form.submit();
    return false;  	
  };
}

수정, 삭제, 다음 글, 이전 글, 목록, 새 글쓰기 버튼

수정, 삭제, 다음 글, 이전 글, 목록, 새 글쓰기 버튼
관련 코드를 다음과 같이 수정한다. (두 군데 모두 수정한다)

<div class="view-menu" ..>
  <security:authorize access="#email == principal.username or hasRole('ROLE_ADMIN')">
  <div class="fl">
    <input type="button" value="수정" class="goModify" />
    <input type="button" value="삭제" class="goDelete" />
  </div>
  </security:authorize>
  <div class="fr">
  <c:if test="${nextArticle != null }">
    <input type="button" value="다음 글" title="${nextArticle.articleNo }" class="next-article" />
  </c:if>
  <c:if test="${prevArticle != null }">
    <input type="button" value="이전 글" title="${prevArticle.articleNo }" class="prev-article" />
  </c:if>
    <input type="button" value="목록" class="goList" />
    <input type="button" value="새 글쓰기" class="goWrite" />
  </div>
</div>

initPage() 함수에 다음 코드를 추가한다.

//수정버튼
var modifyBtns = document.getElementsByClassName("goModify");
i = modifyBtns.length;
while (i--) {
  modifyBtns[i].onclick = function() {
    var form = document.getElementById("modifyForm");
    form.submit();
  };
}
//삭제버튼
var deleteBtns = document.getElementsByClassName("goDelete");
i = deleteBtns.length;
while (i--) {
  deleteBtns[i].onclick = function() {
    var chk = confirm('정말로 삭제하겠습니까?');
    if (chk === true) {
      var form = document.getElementById("delForm");
      form.submit();
    }
  };
}
//다음 글 버튼
var nextArticleBtns = document.getElementsByClassName("next-article");
i = nextArticleBtns.length;
while (i--) {
  nextArticleBtns[i].onclick = function() {
  	    var form = document.getElementById("viewForm");
  	    form.articleNo.value = this.title;
  	    form.submit();
  };
}
//이전 글 버튼
var prevArticleBtns = document.getElementsByClassName("prev-article");
i = prevArticleBtns.length;
while (i--) {
  prevArticleBtns[i].onclick = function() {
    var form = document.getElementById("viewForm");
    form.articleNo.value = this.title;
    form.submit();
  };
}
//목록버튼
var listBtns = document.getElementsByClassName("goList");
i = listBtns.length
while (i--) {
  listBtns[i].onclick = function() {
    var form = document.getElementById("listForm");
    form.submit();
  };
}  
//새 글쓰기 버튼
var writeBtns = document.getElementById("goWrite");
i = writeBtns.length;
while(i--) {
  writeBtns[i].onclick = function() {
      var form = document.getElementById("writeForm");
      form.submit();
  };
}

제목 링크, 페이징 직접 이동 링크, 새 글쓰기 버튼

view.jsp에서 목록과 페이징 처리 부분, 새 글쓰기 버튼
관련 코드를 수정한다.

<table id="list-table" class="bbs-table">
<tr>
  <th style="width: 60px">NO</th>
  <th>TITLE</th>
  <th style="width: 84px;">DATE</th>
  <th style="width: 60px;">HIT</th>
</tr>
<!--  반복 구간 시작 -->
<c:forEach var="article" items="${list }" varStatus="status">
<tr>
  <td style="text-align: center;">
  <c:choose>
    <c:when test="${param.articleNo == article.articleNo }">
      <img src="/resources/images/arrow.gif" alt="현재 글" />
    </c:when>
    <c:otherwise> 
      ${listItemNo - status.index }
    </c:otherwise>
  </c:choose> 
  </td>
  <td>
    <a href="#" title="${article.articleNo }">${article.title }</a>
    <c:if test="${article.attachFileNum > 0 }">
      <img src="/resources/images/attach.png" alt="첨부 파일" style="vertical-align: middle;" />
    </c:if>
    <c:if test="${article.commentNum > 0 }">
      <span class="bbs-strong">[${article.commentNum }]</span>
    </c:if>
  </td>
  <td style="text-align: center;">
    <fmt:formatDate pattern="yyyy.MM.dd" value="${article.regdate }" />
  </td>
  <td style="text-align: center;">${article.hit }</td>
</tr>
</c:forEach>
<!--  반복 구간 끝 -->
</table>
    
<div id="paging">
  <c:if test="${prevPage > 0 }">
    <a href="#" title="${prevPage }">[이전]</a>
  </c:if>
  <c:forEach var="i" begin="${firstPage }" end="${lastPage }">
    <c:choose>
      <c:when test="${param.page == i }">
        <span class="bbs-strong">${i }</span>
      </c:when>
      <c:otherwise>   
        <a href="#" title="${i }">${i }</a>
        </c:otherwise>
    </c:choose>   
  </c:forEach>
  <c:if test="${nextPage > 0 }">
    <a href="#" title="${nextPage }">[다음]</a>
  </c:if>
</div>
<div id="list-menu">
  <input type="button" value="새 글쓰기" />
</div>

initPage() 함수에 다음 코드를 추가한다.

//상세보기에서 목록 제목 링크
var listTable = document.getElementById("list-table");
links = listTable.getElementsByTagName("a");
for (i = 0; i < links.length; i++) {
  links[i].onclick = function() {
    var form = document.getElementById("viewForm");
    form.articleNo.value = this.title;
    form.submit();
    return false;
  };
}
//페이징 처리
var paging = document.getElementById("paging");
links = paging.getElementsByTagName("a");
for (i = 0; i < links.length; i++) {
  links[i].onclick = function() {
    var form = document.getElementById("listForm");
    form.page.value = this.title;
    form.submit();
    return false;
  };
}
//검색 버튼 위 새 글쓰기 버튼
var listMenu = document.getElementById("list-menu");
writeBtn = listMenu.getElementsByTagName("input")[0];
writeBtn.onclick = function() {
  var form = document.getElementById("writeForm");
  form.submit();
};

첨부 파일 링크와 첨부 파일 삭제 링크(jQuery)

자바스크립트 분리 DOM 실습을 jQuery로 수정한다.
순서대로 첨부 파일 다운로드 링크와 삭제 링크부터 수정한다.
첨부 파일링크와 첨부 파일 삭제 링크
jQuery.com에서 jQuery 최신 버전을 내려받는다.
/js 폴더에 내려받은 jQuery 파일을 복사한다.
view.jsp의 <head>와 </head> 사이에 다음 코드를 추가한다.

<script src="/resources/js/jquery-3.3.1.min.js"></script>

다운로드 링크와 삭제 링크는 제이쿼리를 적용하는 데 있어 고칠 부분이 없다.

<div id="detail">
	<div id="date-writer-hit">
		edited <fmt:formatDate pattern="yyyy.MM.dd HH:mm:ss" value="${regdate }" /> by ${name } hit {hit }
	</div>
	<div id="article-content">${content }</div>
	<div id="file-list" style="text-align: right;">
	<c:forEach var="file" items="${attachFileList }" varStatus="status">
		<div class="attach-file">
			<a href="#" title="${file.filename }" class="download">${file.filename }</a>
			<security:authorize access="#email == principal.username or hasRole('ROLE_ADMIN')">
				<a href="#" title="${file.attachFileNo }">x</a>
			</security:authorize>
		</div>
    </c:forEach>
	</div>        
</div>

자바스크립트 기존 내용은 모두 주석 처리한 후 아래 코드를 추가한다.

$(document).ready(function() {
  $('#file-list a.download').click(function(e) {
    e.preventDefault();
    var filename = this.title;
    $('#downForm input[name*=filename]').val(filename);
    $('#downForm').submit();
  });
  $('#file-list a:not(.download)').click(function(e) {
    e.preventDefault();
    var chk = confirm("정말로 삭제하겠습니까?");
    if (chk === true) {
      var attachFileNo = this.title;
      $('#deleteAttachFileForm input[name*=attachFileNo]').val(attachFileNo);
      $('#deleteAttachFileForm').submit();
    }
  });
});

댓글(jQuery)

댓글 반복
다음과 같이 수정한다.

<div id="all-comments">
  <c:forEach var="comment" items="${commentList }">
  <div class="comments">
    <span class="writer">${comment.username }</span>
    <span class="date">${comment.regdate }</span>
    <c:if test="${user.username == comment.username }" >
    <span class="modify-del">
      <a href="#" class="comment-modify-link">수정</a> | 
      <a href="#" class="comment-delete-link" title="${comment.commentNo }">삭제</a>
    </span>
    </c:if>
    <div class="comment-memo">${comment.memo }</div>
    <form class="comment-form" action="updateComment" method="post" style="display: none">
      <input type="hidden" name="commentNo" value="${comment.commentNo }" />
      <input type="hidden" name="boardCd" value="${param.boardCd }" />
      <input type="hidden" name="articleNo" value="${param.articleNo }" />
      <input type="hidden" name="page" value="${param.page }" />
      <input type="hidden" name="searchWord" value="${param.searchWord }" />
      <div style="text-align: right;">
        <a href="#" class="comment-modify-submit-link">전송</a> | <a href="#" class="comment-modify-cancel-link">취소</a>
      </div>
      <div>
        <textarea class="comment-textarea" name="memo" rows="7" cols="50">${comment.memo }</textarea>
      </div>
    </form>
  </div>
  </c:forEach>
</div>

$(document).ready(function() {} 함수에 다음 코드를 추가한다.

//댓글반복
$('.comments').click(function(e) {
  e.preventDefault();
  if ($(e.target).is('.comment-modify-link')) {
    var $form = $(e.target).parent().parent().find('.comment-form');
    var $div = $(e.target).parent().parent().find('.comment-memo');
    if ($form.is(':hidden') === true) {
      $form.show();
      $div.hide();
    } else {
      $form.hide();
      $div.show();
    }
  } else if ($(e.target).is('.comment-modify-cancel-link')) {
    var $form = $(e.target).parent().parent().parent().find('.comment-form');
    var $div = $(e.target).parent().parent().parent().find('.comment-memo');
    if ($form.is(':hidden') === true) {
      $form.show();
      $div.hide();
    } else {
      $form.hide();
      $div.show();
    }
  } else if ($(e.target).is('.comment-modify-submit-link')) {
    var $form = $(e.target).parent().parent().parent().find('.comment-form');
    $form.submit();
  } else if ($(e.target).is('.comment-delete-link')) {
    var chk = confirm('정말로 삭제하겠습니까?');
    if (chk === false) {
      return;
    }
    var $commentNo = $(e.target).attr('title');
    $('#deleteCommentForm input[name*=commentNo]').val($commentNo);
    $('#deleteCommentForm').submit();
  }
});  

이전 글 링크와 다음 글 링크(jQuery)

다음 글 이전 글 링크
관련 코드는 전과 같다.

<div id="next-prev">
  <c:if test="${nextArticle != null }">
    <p>다음 글 : <a href="#" title="${nextArticle.articleNo }">${nextArticle.title }</a></p>
  </c:if>
  <c:if test="${prevArticle != null }">
    <p>이전 글 : <a href="#" title="${prevArticle.articleNo }">${prevArticle.title }</a></p>
  </c:if>
</div>

$(document).ready(function() {} 함수에 다음 코드를 추가한다.

$('#next-prev a').click(function(e) {
  e.preventDefault();
  var articleNo = this.title;
  $('#viewForm input[name*=articleNo]').val(articleNo);
  $('#viewForm').submit();
});

수정, 삭제, 다음 글, 이전 글, 목록, 새 글쓰기 버튼(jQuery)

수정, 삭제, 다음 글, 이전 글, 목록, 새 글쓰기 버튼
관련 코드는 전과 같다.

<div class="view-menu" .. >
  <security:authorize access="#email == principal.username or hasRole('ROLE_ADMIN')">
  <div class="fl">
    <input type="button" value="수정" class="goModify" />
    <input type="button" value="삭제" class="goDelete" />
  </div>
  </security:authorize>    
  <div class="fr">
  <c:if test="${nextArticle != null }">      
    <input type="button" value="다음 글" title="${nextArticle.articleNo }" class="next-article" />
  </c:if>
  <c:if test="${prevArticle != null }">          
    <input type="button" value="이전 글" title="${prevArticle.articleNo }" class="prev-article" />
  </c:if>
    <input type="button" value="목록" class="goList" />
    <input type="button" value="새 글쓰기" class="goWrite" />
  </div>
</div>

$(document).ready(function() {} 함수에 다음 코드를 추가한다.

//수정 버튼
$('.goModify').click(function() {
  $('#modifyForm').submit();
});
//삭제 버튼
$('.goDelete').click(function() {
  var chk = confirm('정말로 삭제하겠습니까?');
  if (chk === true) {
    $('#delForm').submit();
  }
});
//다음 글 버튼
$('.next-article').click(function() {
  var articleNo = this.title;
  $('#viewForm input[name*articleNo]').val(articleNo);
  $('#viewForm').submit();
});
//이전 글 버튼
$('.prev-article').click(function() {
  var articleNo = this.title;
  $('#viewForm input[name*articleNo]').val(articleNo);
  $('#viewForm').submit();
});
//목록버튼
$('.goList').click(function() {
  $('#listForm').submit();
});
//새 글쓰기 버튼
$('.goWrite').click(function() {
  $('#writeForm').submit();
});

제목 링크, 페이징 직접 이동 링크, 새 글쓰기 버튼(jQuery)

view.jsp에서 목록과 페이징처리 부분,새 글쓰기 버튼
관련 코드는 전과 같다.

<table id="list-table" class="bbs-table">
<tr>
  <th style="width: 60px">NO</th>
  <th>TITLE</th>
  <th style="width: 84px;">DATE</th>
  <th style="width: 60px;">HIT</th>
</tr>
<!--  반복 구간 시작 -->
<c:forEach var="article" items="${list }" varStatus="status">
<tr>
  <td style="text-align: center;">
  <c:choose>
    <c:when test="${param.articleNo == article.articleNo }">
      <img src="/resources/images/arrow.gif" alt="현재 글" />
    </c:when>
    <c:otherwise> 
      ${listItemNo - status.index }
    </c:otherwise>
  </c:choose> 
  </td>
  <td>
    <a href="#" title="${article.articleNo }">${article.title }</a>
    <c:if test="${article.attachFileNum > 0 }">
      <img src="/resources/images/attach.png" alt="첨부 파일" style="vertical-align: middle;" />
    </c:if>
    <c:if test="${article.commentNum > 0 }">
      <span class="bbs-strong">[${article.commentNum }]</span>
    </c:if>
  </td>
  <td style="text-align: center;">
    <fmt:formatDate pattern="yyyy.MM.dd" value="${article.regdate }" />
  </td>
  <td style="text-align: center;">${article.hit }</td>
</tr>
</c:forEach>
<!--  반복 구간 끝 -->
</table>
    
<div id="paging">
  <c:if test="${prevPage > 0 }">
    <a href="#" title="${prevPage }">[이전]</a>
  </c:if>
    
  <c:forEach var="i" begin="${firstPage }" end="${lastPage }">
    <c:choose>
      <c:when test="${param.page == i }">
        <span class="bbs-strong">${i }</span>
      </c:when>
      <c:otherwise>   
        <a href="#" title="${i }">${i }</a>
        </c:otherwise>
    </c:choose>   
  </c:forEach>
    
  <c:if test="${nextPage > 0 }">
    <a href="#" title="${nextPage }">[다음]</a>
  </c:if>
    
</div>

<div id="list-menu">
  <input type="button" value="새 글쓰기" />
</div>

$(document).ready(function() {} 함수에 다음 코드를 추가한다.

//상세보기에서 목록 제목 링크
$('#list-table a').click(function(e) {
  e.preventDefault();
  var articleNo = this.title;
  $('#viewForm input[name*articleNo]').val(articleNo);
  $('#viewForm').submit();
});
//페이징 처리
$('#paging a').click(function(e) {
  e.preventDefault();
  var page = this.title;
  $('#listForm input[name*=page]').val(page);
  $('#listForm').submit();
});
//검색 버튼 위 새 글쓰기 버튼
$('#list-menu input').click(function() {
  $('#writeForm').submit();
});

DOM 처리 자바스크립트 실습에서 title 속성을 자주 사용했는데 title은 글로벌 속성이라 다양한 엘리먼트에 쓸 수 있기에 선택했다. 그 외 특별한 이유는 없다.
순수 자바스크립트 또는 jQuery를 사용할 지는 여러분 몫이다.

참고