책 스프링부트와 AWS로 혼자 구현하는 웹서비스
따라하기
어제 JPA
Hibernate
Spring Data Jpa
에 대해 공부했다.
JPA
의 더티 체킹으로 update 쿼리 없이 테이블을 수정해보고
Spring Data Jpa
로 관계형 데이터베이스를 객체지향적으로 관리해보고
JPA Auditing
을 이용하여 등록/수정 시간을 자동으로 기록해봤다.
ORM 은 데이터베이스를 사용하는 서비스를 객체지향적으로 구현하는데 큰 도움을 주는 도구입니다.
열심히 공부해보겠씁니다!
오늘은 머스테치 Mustache
를 이용해 화면 영역을 개발해볼 겁니다.
서버 템플릿 엔진과 클라이언트 템플릿 엔진으 ㅣ차이는 무엇인지, 왜 이책이 JSP 가 아닌 머스테치를 선택했는지, 머스테치를 통해 기본적인 CRUD 화면 개발 방법 등을 차례로 진행해보겠습니다.
좋습니다 시작하시지여
우선
템플릿 엔진이란?
일반적으로 웹 개발에 있어 템플릿 엔진이란, 지정된 템플리 ㅅ양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어를 얘기합니다. JSP, Freemarker 가 떠오를 수도 있고, React, Vue의 View 가 떠오를 수도 있습니다.
맞습니다. React 말하는 건가? 싶었습니다.
둘 모두 결과적으로 지정된 템플릿과 데이터를 이용해 HTML을 생성하는 템플릿 엔진입니다.
오호...
하지만 조금의 차이가 있습니다. 전자(JSP, Freemarker)는 서버 템플릿 엔진이라 불리며, 후자(React, Vue)는 클라이언트 템플릿 엔진이라 불립니다. 개발을 시작하는 많은 개발자들이 이 둘간에 많은 오해를 합니다.
해서 '자바스크립트에서 JSP나 Freemarker처럼 자바 코드를 사용할 순 없나요?' 라는 질문이 커뮤니티에 올라오기도 합니다.
결론부터 말하면FE 의 JS(Nodejs가 아닙니다)가 작동하는 영역
과JSP 가 작동하는 영역
이 다르기 때문인데 JSP를 비롯한 서버 템플릿 엔진은 서버에서 구동되므로, 서버에서 Java 코드로 문자열을 만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달합니다. 이 때 JS코드는 단순한 문자열일 뿐이므로 코드로서 작동하지 않습니다.
호오...전혀 몰랐던 사실. 나도 공부 헛하면 저런 질문 올렸을 거 같다.
이거 이렇게도 할 수 있지 않을까? 하면서 혼자 시간낭비 하고 있었을지도...
JS 의 경우(React, Vue) 브라우저 위에서 작동하므로 서버에서는 Json 이나 Xml 형태의 데이터만 전달하고 클라이언트에서 받은 데이터를 조립하게 됩니다.
물론 최근엔 React 나 Vue 같은 자바 스크립트 프레임워크에서도 서버사이드 랜더링을 지원하는 모습을 볼 수 있지만, 이 책에서 다루지는 않겠습니다.
왜죠....알고싶어요... ㅠ
더 알아봐야겠다 서버사이드랜더링을 선생님이 말해주셨던 거 같다 그래서 공부해봐야겠다.
간단하게 설명하자면 자바 스크립트 프레임워크의 화면 생성 방식을 서버에서 실행하는 것을 이야기 합니다. V8 엔진 라이브러리들이 지원하기 때문입니다. 스프링부트에서 사용할 수 있는 대표적인 기술로는
Nashorn
,J2V8
이 있습니다.
다만 스프링 부트를 사용하면서 JS 를 서버사이드에서 랜더링하도록 구현하는 것은 많은 수고가 필요하므로 시작하는 단계에서는 추천하지 않습니다. 스프링 부트에 대한 이해도와 자바스크립트 프레임워크 양쪽 모두에 대한 이해도가 높아졌을 때 시도해보세요!
넵! 이 책 두 번 보고 바로 도전하겠습니다!
머스테치
수많은 언어를 지원하는 템플릿 엔진. 자바에서 사용될 때는 서버 템플릿 엔진으로, JS 에서 사용될 때는 클라이언트 템플릿 엔진으로 모두 사용할 수 있다고합니다.
저자가 생각한 각 템플릿 엔진
JSP
,Velocity
: 스프링 부트에서는 권장하지 않는 템플릿 엔진Freemarker
: 템플릿 엔진으로는 너무 과하게 많은 기능을 지원합니다. 높은 자유도로 인해 숙련도가 낮을 수록 Freemarker 안에 비즈니스 로직이 추가될 확률이 높습니다.Thymeleaf
: 스프링 진영에서 적극적으로 밀고 있지만 문법이 어렵습니다. HTML 태그에 속성으로 템플릿 기능을 사용하는 방식이 기존 개발자분들께 높은 허들로 느껴지는 경우가 많습니다. Vue 를 사용해본 경험이 있어 태그 속성 방식이 익숙하다면 선택해도 됩니다.Mustache
: 문법 심플, 로직 코드를 사용할 수 없기 때문에 View 의 역할과 서버의 역할이 명확하게 분리됩니다. 무엇보다인텔리제이 커뮤니티(IntelliJ Community)
에서도 플러그인 설치로 간단히 사용할 수 있다.(Thymeleaf
,JSP
는인텔리제이 얼티메이트(Ultimate)
에서만 지원한다)
템플릿 엔진이 복잡하게 되면 서로 로직을 나눠갖게 되어 유지보수가 굉장히 어렵습니다.
mustache
를 검색한다.build.gradle
에 의존성을 등록해준다.// build.gradle
...
implementation('org.springframework.boot:spring-boot-starter-mustache')
...
보이는 것처럼 머스타치는 스프링부트에서 공식 지원하는 템플릿 엔진입니다. 의존성하나만추가하면 다른 스타터 패키지와 마찬가지로 추가 설정없이 설치가 끝납니다. 별도 버전을 신경쓰지 않아도 되는 장점!
index.mustaches
파일을 만들어준다.이 위치에 머스타치 파일을 두면 스프링 부트에서 자동으로 로딩한다고 한다.
// index.mustaches
<!DOCTYPE HTML>
<html>
<head>
<title>스프링 부트 웹서비스</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>스프링 부트로 시작하는 웹 서비스</h1>
</body>
</html>
여담인데 이거는 단축키 없나 vscode 는 rafce 하면 기본적으로 세팅되잖아. 그런거말이야.
IndexController.java
를 만들어준다.(dto 안이 아님 web 임)// IndexController.java
package com.prac.webservice.springboot.web;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class IndexControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void 메인페이지_로딩() {
// when
String body = this.restTemplate.getForObject("/", String.class);
// then
assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
}
}
❌
build.gradle 에 spring.framework 라고 써버렸다.
❌
test h1 태그에 '웹서비스' 라고 붙여서 써버렸다. 원본엔 '웹 서비스' 라고 띄워놓았다. 그래서 같은 문자열이 없으니 에러발생
이번 테스트는 실제로 url 호출 시 페이지의 내용이 제대로 호출되는 지에 대한 테스트 입니다.
/
을 호출했을 때index.mustache
에 포함된 코드들이 있는지 확인하면 됩니다. 하지만 뭔가 아쉬우니 화면이 잘 나오는 확인해봅시다.
좋습니다. 화면으로 확인해보면 더 좋습니다.
Application.java
를 실행시킨 뒤에 localhost:8080
에 접속해보면 이게 표시된다. 만약 html 을 적어넣었다면 html 이 표시됐겠지?
PostsApiController 로 API는 구현해놓았으니 바로 화면을 개발해봅시다. 하지만 그냥 HTML만 사용하기에는 멋이 없으니, 오픈소스인 부트스트랩을 이용해 화면을 만들어봅시다.
처음 들어보는 것 같다. 부트스트랩.
부트스트랩, 제이쿼리 같은 프론트엔드 라이브러리를 사용할 수 있는 방법은 크게 2가지가 있습니다. 하나는 외부 CDN 을 사용하는 것이고 하나는 직접 라이브러리를 받아서 사용하는 것입니다. 우리는 외부 CDN 을 사용할 겁니다. 직접 받을 필요없고, 사용방법도 HTML/JSP/Mustache 에 코드 한 줄만 추가하면 되니 굉장히 간단합니다.
✔실제 서비스에서는 이 방법을 잘 사용하지 않습니다. 결국은 외부 서비스에 우리 서비스가 의존하게 되어버리는 꼴이라, CDN을 서비스하는 곳에 문제가 생기면 덩달아 같이 문제가 생기기 때문입니다.
일단, 2개의 라이브러리 부트스트랩과 제이쿼리를 index.mustache 에 추가해야합니다. 하지만 우리는 바로 추가하지 않고 레이아웃 방식으로 추가해보겠습니다 * 레이아웃 방식? 공통 영역을 별도의 파일로 분리하여 필요한 곳에서 가져다 쓰는 방식
매번 해당 라이브러리를 머스타치 파일에 추가하는 것은 번거로우니 레이아웃 파일들을 만들어 추가합니다.
layout
디렉토리를 추가로 생성하고 header.mustache
, footer.mustache
파일을 만든다.<!-- header.mustache -->
<!DOCTYPE HTML>
<html>
<head>
<title>스프링부트 웹서비스</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
<!-- footer.mustache -->
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
</body>
</html>
너무 신기하다. 코드가.
서로 연결된 파일이 아닌데 header부분에서 body가 시작되고 footer 에서 body 가 끝나고 html 이 끝난다.
그리고 저자의 github 에 가서 index 파일을 보면 더 신기하다.
css 와 js 의 위치가 다른데, 페이지 로딩속도를 높이기 위해 css 는 header 에 js 는 footer 에 넣었습니다. HTML 은 위에서부터 아래로 코드가 실행되므로 head 가 다 실행되고 나서 body 가 실행됩니다. 그래서 head 가 로딩되지 않으면 사용자는 백지만 보게됩니다. 그래서 용량이 큰 js 는 body 하단에 둬 화면이 다 그려진 뒤에 호출하는 것이 좋습니다. css 가 불러와지지 않으면 깨진 화면을 사용자가볼 수 있으므로 head 에서 불러옵니다.
오 맞는 말이야 참 신기해.
추가로, bootstrap.js 는 제이쿼리가 꼭 있어야만 하기 때문에 부트스트랩보다 먼저 호출되도록 코드를 작성했습니다. 이런 상황을 보통 부트스트랩이 제이쿼리에 의존한다. 고 합니다.
이해가 잘 안되지만, 라이브러리를 비롯해 HTML 태그들이 모두 레이아웃에 추가되었으니 index.mustache 에는 필요한 코드만 남고
<!-- index.mustache -->
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹 서비스</h1>
...
{{>layout/footer}}
{{>}}
는 현재 머스테치 파일을 기준으로 다른 파일을 가져옵니다.
위 아래에서 layout 의 header 를 가져오고 footer 를 가져오는 거다.
<!-- index.mustache -->
{{>layout/header}}
<h1>스프링부트로 시작하는 웹 서비스 Ver.2</h1>
<div class="col-md-12">
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
</div>
</div>
</div>
{{>layout/footer}}
글 등록 을 누르면 /posts/save
로 이동하는 코드다.
이 주소에 해당하는 Controller 를 만들어보자.
페이지에 관련된 컨트롤러는 모두
IndexController
를 사용합니다.
// IndexController.java
// 클래스 위에 @RequiredArgsConstructor 를 추가하고
...
@GetMapping("/posts/save")
public String postsSave() {
return "posts-save";
}
}
index.mustache
같이 /posts/save
를 호출하면 posts-save.mustache
가 호출된다.
아니 어떻게 return "posts-save"
를 하는데 posts-save.mustache
가 호출되지? 알고 싶다.
너무 기니까 저자의 깃허브 에서 복붙하자.
아니 생각해보면 지금 쓰는 블로그 글은 내 코드 복붙하면 되네?
<!-- posts-save.mustache -->
{{>layout/header}}
<h1>게시글 등록</h1>
<div class="col-md-12">
<div class="col-md-4">
<form>
<div class="form-group">
<label for="title">제목</label>
<input type="text" class="form-control" id="title" placeholder="제목을 입력하세요">
</div>
<div class="form-group">
<label for="author"> 작성자 </label>
<input type="text" class="form-control" id="author" placeholder="작성자를 입력하세요">
</div>
<div class="form-group">
<label for="content"> 내용 </label>
<textarea class="form-control" id="content" placeholder="내용을 입력하세요"></textarea>
</div>
</form>
<a href="/" role="button" class="btn btn-secondary">취소</a>
<button type="button" class="btn btn-primary" id="btn-save">등록</button>
</div>
</div>
{{>layout/footer}}
localhost:8080/
으로 접속해보자.
어머 글 등록이라는 버튼이 생겼네?!
버튼이 생긴건 호들갑 떨 일이 아니다.
버튼을 누르고 posts-save 로 옮겨졌을 때 호들갑 떨어야한다.
미쳤다...!
하지만 아직 등록버튼은 작동하지 않습니다.
??
뭐예요 ;; 빨리 작동되게 해줘요 ;;
API 를 호출하는 JS 가 전혀 없기 때문입니다. 그래서 src / main / resources 에 static / js / app 디렉토리를 만듭니다.
전혀 이해하지 못했다. API?
API 가 뭔지 이해 못한 것 같다 나는.
JS 가 없다는 건 이해했다.
이게 무슨소리야...?
인텔리제이 커뮤니티는 자바스크립트 지원 안 해...?
속상하다. 일단 코드를 적어보자.
// index.js
var index = {
init : function () {
var _this = this;
$('#btn-save').on('click', function () {
_this.save();
});
},
save : function () {
var data = {
title: $('#title').val(),
author: $('#author').val(),
content: $('#content').val()
};
$.ajax({
type: 'POST',
url: '/api/v1/posts',
dataType: 'json',
contentType:'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function() {
alert('글이 등록되었습니다.');
window.location.href = '/';
}).fail(function (error){
alert(JSON.stringify(error));
});
}
};
index.init();
done 메서드는 글 등록이 성공하면 alert 로 글이 등록되었다는 말을 띄우고 다시 '/' 로 메인으로 돌아가게 한다.
fail 은 에러를 띄운다.
footer.mustache 에 코드를 추가해주자
<!-- footer.mustache -->
...
<!--index.js 추가-->
<script src="/js/app/index.js"></script>
</body>
</html>
서버를 실행하고 localhost:8080 에 들어가서 버튼을 누르고 게시글을 작성하고 등록
버튼을 누른다.
아냐 우리 저자님이 실수할 수도 있지 ㅡㅡ
그 기능을 알지 못하고 수정하지 못한 내가 잘못한 거지
index.js 의 코드는
var index
로 시작해야한다. 책은var main
으로 시작함 ;;
아니해야한다
보다는 위 아래가 맞아야 한다. 시작이 main 이면 바닥에 index.init(); 이main.init();
으로 바뀌어야 한다. 아니면 시작을 var index 로 바꾸어야 하지.
시작은var main
, 마무리는index.init
으로 적어서 실행하면
마지막에 index 가 정의되지 않았다고 하면서 posts 페이지로 넘어가면 바로 에러가 발생해있다.
별거 아니다. 하지만 js 를 모르면 이해할 수 없다.
나는 몰랐으니 그냥 오타를 고치는 수준이다.
done 인데 don 이라고 적어넣음. 그래서 에러가 표시됐다.
오타만 고치면 된다.
꺅!!!
확인 버튼을 누르면 메인페이지로 잘 이동한다.
localhost:8080/h2-console
로 들어가 조회해보자.
SELECT * FROM posts
등록도 매우 잘 된 것 같다.
이해한 거 없이 거의따라 적기만 했다.
힘들다.
내일은 조회를 해보자.