어디까지 갈 수 있을까?
머스테치로 화면 구성하기 본문
템플릿 엔진
지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어
스프링에서 권장하는 템플릿 엔진에는 머스테치, Thymeleaf가 있다
서버 템플릿 엔진과 클라이언트 템플릿 엔진
리액트, 뷰 - 클라이언트 템플릿 엔진
서버에서 일단 빈 html을 보내고 js를 다 읽고 화면을 그림
단 : 첫 화면을 보기까지 걸리는 시간이 길다
Jsp, Freemarker - 서버 템플릿 엔진
서버에서 모든 데이터를 만들어 클라이언트에 html만 보냄
기본 페이지 만들기
1. build.gradle에 의존성 추가
compile 'org.springframework.boot:spring-boot-starter-mustache'
2. index.mustache 생성
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>스프링 부트 웹 서비스</title>
</head>
<body>
<h1>스프링 부트로 시작하는 웹서비스</h1>
</body>
</html>
3. IndexController 생성
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
머스테치 스타터 덕분에 return "index" 를 하면
View Resolver가 src/main/resources/templates/index.mustache 파일 반환
4. IndexControllerTest 클래스 생성
@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("스프링 부트로 시작하는 웹서비스");
}
}
@TestRestTemplate
HTTP 요청 후 Json, xml, String 과 같은 응답을 받을 수 있는 템플릿
5. 결과
게시글 등록 페이지 만들기
1. 공통 코드 추가하기
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>
<!--index.js 추가-->
<script src="/js/app/index.js"></script>
</body>
</html>
자바스크립트의 절대경로는 static부터 시작됨
페이지 로딩속도를 높이기 위해 header에는 css, footer에는 js를 둔다
2. 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}}
{{>layout/header}}
현재 머스테치 파일을 기준으로 다른 파일을 가져옴
3. 페이지 및 컨트롤러 생성
4. 게시글 등록 버튼 기능 만들기
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();
footer에 index.js 추가하면 해당 코드가 동작한다
브라우저의 스코프는 공용 공간으로 쓰이기 때문에 전역변수를 사용하면 브라우저의 전역 변수 충돌 문제가 발생한다.
중복된 함수 이름을 피하기 위해 객체 안에 객체를 만들어 index 객체 안에서만 함수를 유효하게 만든다
html->js->controller->service->repository->db 순으로 가서 데이터가 저장된다
전체 조회 화면 만들기
1. index.mustache UI 변경
{{>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>
<br>
<!-- 목록 출력 영역 -->
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
{{#posts}}
<tr>
<td>{{id}}</td>
<td><a href="/posts/update/{{id}}">{{title}}</a></td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
</table>
</div>
{{>layout/footer}}
{{#posts}}
posts 라는 List를 순회함
자바의 for 문과 동일
{{id}}
Lists에서 뽑아낸 객체의 id 필드를 사용
2. PostsRepository 쿼리 추가
public interface PostsRepository extends JpaRepository<Posts, Long> {
@Query("SELECT p FROM Posts p ORDER BY p.id DESC")
List<Posts> findAllDesc();
}
@Query가 가독성이 좋으니 선택해서 사용
Posts 라는 도메인 객체에 p라는 별칭을 붙여 객체의 전체 필드를 가져오겠다는 뜻
네이티브 쿼리가 아니라 JPA에서 사용되는 JPQL 이다
3. PostsService에 코드 추가
@Transactional(readOnly = true)
public List<PostsListResponseDto> findAllDesc() {
return postsRepository.findAllDesc().stream()
.map(PostsListRequestDto::new)
.collect(Collectors.toList());
}
@Transactional에 readOnly=true 옵션을 줘, 조회 기능만 사용해 조회 속도를 개선시킨다.
.map(PostsListResponseDto::new) 는
.map(posts -> new PostsListResponseDto(posts) 와 같다
postsRepository 결과로 넘어온 Posts의 stream을 map을 통해 PostsListResponseDto 변환 -> List로 반환하는 코드
4.PostsListRequestDto 생성
@Getter
public class PostsListResponseDto {
private Long id;
private String title;
private String author;
private LocalDateTime modifiedDate;
public PostsListResponseDto(Posts entity) {
this.id = entity.getId();
this.title = entity.getTitle();
this.author = entity.getAuthor();
this.modifiedDate = entity.getModifiedDate();
}
}
5. Index.Controller 변경
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postService;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("posts", postService.findAllDesc());
return "index";
}
@GetMapping("/posts/save")
public String postsSave() {
return "posts-save";
}
}
Model
객체를 저장할 수 있다
여기서는 postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache로 전달
6. 결과
게시글 수정
1. PostsApiController.ajva
@PutMapping("/api/v1/posts/{id}")
public Long update(@PathVariable Long id,
@RequestBody PostsUpdateRequestDto requestDto) {
return postsService.update(id, requestDto);
}
해당 api 주소로 요청하는 화면 개발
2. 게시글 수정 화면 posts-update.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="id" value="{{post.id}}" readonly>
</div>
<div class="form-group">
<label for="title">제목</label>
<input type="text" class="form-control" id="title" value="{{post.title}}">
</div>
<div class="form-group">
<label for="author"> 작성자 </label>
<input type="text" class="form-control" id="author" value="{{post.author}}" readonly>
</div>
<div class="form-group">
<label for="content"> 내용 </label>
<textarea class="form-control" id="content">{{post.content}}</textarea>
</div>
</form>
<a href="/" role="button" class="btn btn-secondary">취소</a>
<button type="button" class="btn btn-primary" id="btn-update">수정</button>
</div>
</div>
{{>layout/footer}}
{{post.id}}
post 객체의 id 필드에 접근하겠다
readonly
input 태그에서 읽기 기능만 허용
3. index.js에 update function 추가
var index = {
init: function () {
var _this = this;
$('#btn-save').on('click', function () {
_this.save();
});
$("#btn-update").on('click', function () {
_this.update();
});
},
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));
});
},
update : function () {
var data = {
title : $("#title").val(),
content : $("#content").val()
};
var id = $("#id").val();
$.ajax({
type : 'PUT',
url : '/api/v1/posts/'+ id,
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();
type : 'PUT'
Controller에서 @PutMapping으로 선언했기 때문에 PUT 사용 해야 함
REST에서 CRUD는 다음과 같이 정의
POST - 생성
GET - 읽기
PUT - 수정
DELETE - 삭제
JSON.stringify()
JavaScript 값이나 객체를 JSON 문자열로 변환
4. 수정 페이지로 이동할 수 있게 index.mustache 수정
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
{{#posts}}
<tr>
<td>{{id}}</td>
<td><a href="/posts/update/{{id}}">{{title}}</a></td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
</table>
5. 수정 화면을 연결할 IndexController에 메소드 생성
@GetMapping("/posts/update/{id}")
public String postsUpdate(@PathVariable Long id, Model model){
PostsResponseDto dto = postService.findById(id);
model.addAttribute("post", dto);
return "posts-update";
}
6. 결과
|
|
실습과정에서 오류가 났다.
index.js 파일을 고쳤는데 웹에서는 적용이 안 돼 수정버튼을 눌러도 동작하지 않았다.
웹은 스크립트 파일을 캐시에 저장하고 사용 시에 불러들이는데 이 때 캐시에 있는 파일이 갱신이 안 돼 일어난 오류라고 한다.
<!--index.js 추가-->
<script src="/js/app/index.js?ver=1"></script>
이 때 footer.mustache 에 위와 같이 ?ver=1 쿼리스트링을 추가해주면 웹이 다른 파일로 인식해 변경사항이 적용된다.
서버를 내리면 h2 데이터베이스에 있는 내용이 사라지는 것에 대해서도 궁금증이 들었는데,
h2는 in-memory와 file based 모드 두 개가 있는데 현재 우리가 쓰는 것은 파일을 저장하지 않고 메모리상에서 사용하는 형태라 서버를 내리면 사라진다고 한다.
게시글 삭제
1. posts-update.mustache 수정
<a href="/" role="button" class="btn btn-secondary">취소</a>
<button type="button" class="btn btn-primary" id="btn-update">수정</button>
<button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
btn-delete 클릭시 JS에서 이벤트를 수신한다
2.index.js 수정
$("#btn-delete").on('click', function () {
_this.delete();
});
...
delete : function () {
var id = $("#id").val();
$.ajax({
type : 'DELETE',
url : '/api/v1/posts/'+ id,
dataType : 'json',
contentType : 'application/json; charset=utf-8'
}).done(function () {
alert('글이 삭제되었습니다.');
window.location.href = '/';
}).fail(function (error) {
alert(JSON.stringify(error));
});
}
3. PostsService.java 추가
@Transactional
public void delete (Long id) {
Posts posts = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 없습니다 id=" + id));
postsRepository.delete(posts);
}
postsRepository.delete(posts);
존재하는 Posts인지 확인을 위해 엔티티 조회 후 삭제
서비스에서 만든 delete 메소드를 컨트롤러가 사용하도록 코드 추가
4. PostsApiController.java
@DeleteMapping("/api/v1/posts/{id}")
public Long delete(@PathVariable Long id) {
postsService.delete(id);
return id;
}
'책 > 스프링부트와 AWS로 혼자 구현하는 웹 서비스' 카테고리의 다른 글
EC2 서버에 프로젝트를 배포해보자 &코드가 푸시되면 자동으로 배포해 보자 (0) | 2021.04.12 |
---|---|
AWS 서버, 데이터베이스 환경을 만들어보자(EC2, RDS) (0) | 2021.04.08 |
스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (0) | 2021.03.25 |
[작성중] 프로젝트 구조 (0) | 2021.03.16 |
테스트 코드, JPA (0) | 2021.03.12 |