이동욱님의 SpringBoot로 웹서비스 출시하기를 보고 공부하기 3번째 포스팅입니다. 이번 포스팅에서는 Handlebars template 엔진을 이용해 간단히 main페이지를 만들고, 게시판 작성기능을 구현하도록 하겠습니다.
1. Handlebars로 화면 만들기
1.1 의존성 추가
build.gradle에 위와 같이 의존성을 추가합니다.(implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0'
)
*SpringBoot 2.X 버젼을 사용하신다면 handlebars의 version을 0.3.X 를 사용해야 합니다.
1.2 main 페이지 작성
이번에는 main 페이지를 작성하겠습니다.
handlebars의 기본 경로는 src/main/resources/templates
입니다. 따라서 위와 같이 src/main/resources/templates 하위에 main.hbs 파일을 추가합니다. (SpringBoot에서 handlebars를 지원하므로 별도의 resolver를 등록하지 않아도 controller에서 template을 찾을 수 있습니다.)
<html lang="en">
<head>
<title>스프링부트 웹 서비스</title>
<meta charset="UTF-8">
<meta name ="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<h1>Spring Boot Web Service</h1>
</body>
</html>
그 후 위와 같이 작성합니다.
1.3 Controller 추가
main를 응답해줄 Controller를 작성하겠습니다. 우선 web 패키지 하위에 WebController
class를 추가합니다.
xxxxxxxxxx
public class WebController {
"/") (
public String main(){
return "/main";
}
}
그 후 위의 코드를 추가합니다.
1.4 Controller Test 하기
방금 작성한 WebController를 테스트 하기 위해 src/test/java/galid/com/ldw_study/web/
하위에 WebControllerTest
class를 생성합니다.
xSpringRunner.class) (
webEnvironment = RANDOM_PORT) (
public class WebControllerTest {
private TestRestTemplate restTemplate;
public void 메인페이지_요청(){
//when
String body = this.restTemplate.getForObject("/", String.class);
//then
assertThat(body).contains("Spring Boot Web Service");
}
}
위와 같이 방금 작성한 Controller에 요청을 보내는 테스트 코드를 작성합니다. 위의 코드는 페이지를 요청을 보낸 페이지가 제대로 응답되는지에 대한 테스팅을 하는 코드입니다. '/'
로 요청시 main.hbs
를 응답하도록 했습니다. html도 따지고 보았을때는 어떠한 규격을 따르는 문자열코드 입니다. 따라서 main.hbs에 작성했던 일부내용이 응답페이지에 담겨있는지만 확인하면 됩니다.
테스팅에 성공했습니다 ~
어플리케이션을 실행한 뒤 실제 웹에서 호출해보면 ~ 성공입니다!
2. 게시판 글 작성기능 구현
2.1 Service Class 생성
사용자의 요청에 의해 특정 로직을 처리하는 코드를 보통은 service클래스 라고 합니다. MVC 패턴을 배웠다면 이해하실 수 있을 겁니다.
앞서 작성한 Contoller
에서는 직접 Dao객체(PostsRepository)
를 참조하여 로직을 작성하지 않고, 해당 요청시 처리할 로직을 service
에게 위임하여 처리한 뒤 적절한 view
를 return만 해줍니다. 이렇게 함으로써, Controller와 Service의 역할을 분리하고, Controller는 앞서 말씀드린 적절한 Service 요청, View 반환의 역할만 합니다.
우선 service 패키지를 생성한 뒤 PostsService 클래스를 생성합니다.
xxxxxxxxxx
public class PostsService {
private PostsRepository postsRepository;
public Long save(PostsSaveRequestDto dto){
return postsRepository.save(dto.toEntity()).getId();
}
}
@Service
다들 아시다시피 해당 class를 Bean으로 등록하는 어노테이션입니다.
@Transactional
이 어노테이션은 트랜잭션 처리를 간편하게 도와주는 어노테이션입니다.
위와 같이 하나의 dto만을 등록할때에는 크게 문제가 없지만, 예를 들어 여러개의 dtos를 등록한다고 했을 때, 몇개의 dto만 저장되고 나머지는 저장이 되지 않는다면 큰 문제가 발생할 것입니다. 따라서 모든 dto가 정상저장 되었을때에만 commit하고 예외가 발생하면 rollback해주는 어노테이션 입니다.
DB에 데이터를 등록/수정/삭제 하는 경우에는 필수적으로 이 어노테이션을 부여한다고 합니다.
2.2 Service Test 작성
테스트 디렉토리 하위에 service 패키지를 만들고 PostsServiceTest 클래스를 생성합니다.
xxxxxxxxxx
SpringRunner.class) (
public class PostsServiceTest {
private PostsService postsService;
private PostsRepository postsRepository;
public void cleanup(){
postsRepository.deleteAll();
}
public void Dto데이터가_posts_entity_테이블에_저장된다(){
//given
PostsSaveRequestDto dto = PostsSaveRequestDto.builder()
.title("테스트 타이틀")
.content("테스트 내용")
.author("작성자")
.build();
//when
postsService.save(dto);
//then
List<PostsEntity> postsEntityList = postsRepository.findAll();
PostsEntity postsEntity = postsEntityList.get(0);
assertThat(postsEntity.getTitle()).isEqualTo(dto.getTitle());
assertThat(postsEntity.getAuthor()).isEqualTo(dto.getAuthor());
assertThat(postsEntity.getContent()).isEqualTo(dto.getContent());
}
}
그 후 위와 같이 테스트 코드를 작성합니다. 허나 위와 같이 dto를 생성하기 위해서는 @Builder를 추가해주어야 합니다. 바로 추가하러 가보겠습니다.
xxxxxxxxxx
public class PostsSaveRequestDto {
private String title;
private String content;
private String author;
public PostsEntity toEntity() {
return PostsEntity.builder()
.title(title)
.content(content)
.author(author)
.build();
}
}
@Builder, @AllArgsConstructor
을 추가해주었습니다. @Builder를 사용하기 위해서는 매개변수를 가지는 생성자를 생성해야합니다. 이 때 @AllArgsConstructor을 통해 모든 필드를 매개변수로 가지는 생성자를 생성했습니다. Entity와는 다르게 매개변수로 받지 않아도되는 불필요한 값, 예를들자면 id(auto_increment)와 같은 필드가 없기 때문에 AllArgsConstructor을 사용했습니다.
다시 PostsServiceTest
로 돌아와 테스트를 하면! 성공입니다.
2.3 게시판 글작성 Template 만들기
이번에는 게시판 글작성 template을 만들겠습니다.
2.3.1 Bootstrap(css, js), jquery 설정
css, js 는 부트스트랩을 이용할 것입니다.
위 그림과 같이 bootstrap 홈페이지로 접속하여 css cdn 주소를 복사하고, 페이지 상단의 <head>
태그안에 기입합니다.
부트스트랩의 css들은 javascript들을 기반으로 동작하는 것들이 많기 때문에 별도의 js(javascript 파일)
도 제공해줍니다. 역시 복사합니다. bootstrap에서 제공하는 jquery
의 cdn주소에 문제가 있는것인지 잘 설정을 못하는 것인지는 모르겠지만 문제가 있습니다. 따라서 jqeury의 경우 별도로 jquery 홈페이지에서 얻어올 것입니다.
우선은 아래의 2개의 js주소만을 복사합니다. *위의 bootstrap.min.js
의 경우 jquery
가 꼭 필요하기 때문에 jquery
가 먼저 로딩되어야 합니다. 때문에 jquery
관련 cdn을 더 먼저 작성해야합니다.
또한, js
관련 로딩은 페이지의 body
태그 가장 하단에 적어줍니다. 이렇게 함으로써 페이지 로딩 속도를 높일 수 있습니다. 이유는 이 포스팅의 마지막 부분인 팁
에서 알려드리겠습니다.
위와 같이 jqeury cdn 홈페이지로 접속하여 jqeury 3.x 버젼의 slim을 클릭하면, cdn주소를 제공해줍니다. 이를 복사하여 위의 js들의 가장 상위에 붙혀넣습니다.
2.3.2 글작성 template 작성
글 작성 모달창을 띄우는 간단한 버튼입니다.
- 먼저 class = "col-md-x"
는 bootstrap의 grid 지원 시스템입니다. 다음 링크를 참고해주세요. http://bootstrapk.com/css/#grid
- button의 class="btn btn-primary"
는 위의 그림과 같이 버튼의 디자인을 만들어주는 class 입니다.
- data-toggle="modal"
의 경우 bootstrap에서 지원하는 js입니다. 간단히 말씀드리면 modal창을 띄울 수 있도록 만들어주는 기능입니다.
- data-target="#savePostsModal"
은 #뒤에 적힌 id값에 해당하는 컴포넌트를 modal창으로 띄워주는 역할을 합니다.
x
<html>
<head>
<title>스프링부트 웹서비스</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 부트스트랩 css 추가-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>
<body>
<h1>스프링부트로 시작하는 웹 서비스</h1>
<div class="col-md-12">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#savePostsModal">글 등록</button>
</div>
<div class="modal fade" id="savePostsModal" tabindex="-1" role="dialog" aria-labelledby="savePostsLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="savePostsLabel">게시글 등록</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<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>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">취소</button>
<button type="button" class="btn btn-primary" id="btn-save">등록</button>
</div>
</div>
</div>
</div>
<!-- jquery 추가 -->
<script
src="https://code.jquery.com/jquery-3.4.1.js"
integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="
crossorigin="anonymous"></script>
<!-- 부트스트랩 js 추가 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
다음은 modal 창의 내용을 작성합니다. 앞서 만든 button으로 위의 modal창을 띄우기 위해 id를 savePostsModal
로 맞춥니다. class="modal fade"
은 해당 div 안의 내용들을 modal창으로 인식하게 만듭니다.
결과 화면 입니다. 버튼을 누르면 방금 작성한 div안의 내용이 나타남을 볼 수 있습니다. 현재 등록
버튼에는 아무런 기능을 부여하지 않은 상태입니다. 등록
버튼의 기능을 만들기 위한 js를 만들겠습니다.
resources/static/js/app
하위에 main.js 파일을 생성합니다.
var main = {
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: '/posts',
dataType: 'json',
contentType:'application/json; charset=utf-8',
data: JSON.stringify(data)
}).done(function() {
alert('글이 등록되었습니다.');
location.reload();
}).fail(function (error) {
alert(error);
});
}
};
main.init();
main.js에 위와같이 코드를 작성합니다. main
이라는 객체에 2가지 함수를 변수로 가지게 했습니다.
init()
init() 함수의 경우 main.hbs
의 btn-save
라는 id를 가지는 버튼에 리스너를 추가합니다. 해당 리스너가 호출할 함수는 아래에서 작성할 save()
함수입니다.
save()
save() 함수의 data
변수에는 main.hbs
의 title, author, content
라는 id를 가지는 컴포넌트의 값들을 각각 title, author, content라는 변수에 담습니다. 또, 이 data
변수를 string으로 변환한 뒤 body에 담아 /posts
에 요청을 보냅니다.
마지막으로 main.init()을 호출하여 이 js가 로딩될 때 init()메소드를 실행하여 버튼에 리스너를 등록하도록 했습니다. 각각의 함수를 main이라는 변수안에 따로 생성한 이유는 팁
에서 설명 드리겠습니다.
<!-- jquery 추가 -->
<script
src="https://code.jquery.com/jquery-3.4.1.js"
integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU="
crossorigin="anonymous"></script>
<!-- 부트스트랩 js 추가 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
<!--custom js 추가-->
<script src="/js/app/main.js"></script>
</body>
</html>
방금 작성한 js를 추가해줍니다.
2.3.3 확인
application을 실행하고 글을 등록합니다.
h2 console에 접속하여 확인하면 ? 방금 기입한 데이터가 추가된것을 볼 수 있습니다.
팁
1. Handlebars
1.1 Handlebars를 사용하면 좋은 이유
Springboot에서는 jsp, freemaker, velocity
등의 여타 template 엔진을 지양한다고 합니다. 업데이트가 거의 되지 않고 있기 때문입니다. 반면에 handlebars
는 꾸준한 업데이트를 해오고 있다고 합니다. 또한, 아래의 이유 때문입니다.
- 문법이 간단하다
- 로직 코드를 사용할 수 없기 때문에 View의 역할, Server의 역할이 명확히 구분된다.
1.2 Handlebars Plugin
IDE로 intellij를 사용하고 계시다면, 위의 플러그인을 사용하시는 것을 추천합니다. 문법체크 등의 지원을 받을 수 있다고 합니다. (문법체크 참 중요합니다..)
2. CSS
bootstrap 프레임워크를 이용하는 방식은 CDN 방식, 라이브러리를 직접 다운로드하는 방식
두가지가 존재합니다. 제 포스팅에서는 cdn방식을 이용했습니다만, 실제 배포환경에서는 cdn을 거의 사용하지 않는다고 합니다. 어플리케이션이 cdn서버에 의존하게 되기 때문이라고 합니다.
3. html 작성 팁 (페이지 로딩속도 향상)
앞서 main.hbs에 bootstrap cdn코드와, jqeury cdn 코드의 위치를 나누어 작성한 것을 볼 수 있습니다. 왜그럴까요? 바로 페이지의 로딩속도를 향상시키기 위함입니다.
왜 위아래에 나누어 작성하는 경우 로딩속도가 향상될까요? html의 경우 위에서부터 차례로 코드가 실행되기 때문에, head
의 로딩이 다 되지 않으면 웹 브라우저에서는 백지 화면만이 보이게 됩니다. 따라서 head에서 모든 로딩을 하게 된다면 그만큼 느려지게 됩니다.
css의 경우,
화면을 직접적으로 그리는데 필요한 역할(디자인) 이므로 head에서 로딩해오는 것이 맞습니다.
js의 경우,
기능적인 측면을 담당하기 때문에 화면이 그려진 후 불러오도록 해도 됩니다.
4. js 함수들을 또 변수안에 담는 이유
main.hbs에서 등록
버튼에 기능을 추가하기 위해 main.js
를 작성할 때, var main
이라는 변수에 함수들을 담아서 사용했었습니다. 왜그럴까요? 하나의 js만을 불러사용하는 경우에는 문제가 되지 않지만 main.js에서 또다른 기능이 필요하여 a.js
를 추가한 경우에 문제가 발생됩니다. 물론 특정상황입니다.
그 특정상황이란 다음과 같습니다. main.js
에서 작성한 함수이름과 a.js
의 함수이름이 같은 경우입니다. 브라우저는 scope을 공용으로 사용합니다. 따라서 여러개의 js에서 같은 이름의 함수를 사용하는 경우 가장 마지막에 불려진 js의 것을 사용하게 된다고합니다.
5. Handlebars template작성 예제
'FrameWork > Spring Boot' 카테고리의 다른 글
SpringBoot - SpringBoot H2 연결방법(H2 웹콘솔) (4) | 2019.08.05 |
---|---|
Spring Boot - Self-Signed certificate(자체서명된 인증서)를 이용해 https 구축 (2) | 2019.08.03 |
SpringBoot - Controller에서 return 하는 Object의 field 이름변경 (@JsonProperty) (0) | 2019.06.22 |
SpringBoot - SpringBoot로 웹 개발하기 - 2(API 만들기 , 테스트) (0) | 2019.05.02 |
SpringBoot - SpringBoot로 웹 개발하기 - 1(프로젝트 생성) (0) | 2019.05.02 |