이 글은 '스프링부트와 AWS로 혼자 구현하는 웹 서비스 - 이동욱(jojoldu)'을 공부하며 작성한 글로 생략된 내용은 책을 구매해서 확인하는 것을 권장합니다.
참고 소스코드 깃허브 https://github.com/jojoldu/freelec-springboot2-webservice
http://www.yes24.com/Product/Goods/83849117
스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - YES24
가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현한다. JPA와 JUnit 테스트, 그레이들, 머스테치, 스프링
www.yes24.com
책에 4장에 해당하는 내용 중 게시글 등록 및 전체 조회 화면, js /css 선언 위치를 다르게 하는 이유, js 객체를 만들어 사용하는 이유에 대해 정리하고자 한다.
부트스크랩, 제이쿼리 등 프론트엔드 라이브러리를 사용하는 방법
1. 외부 CDN 사용 (이 프로젝트에서 사용할 방법)
2. 직접 라이브러리를 받아서 사용
실무에서는 2번을 주로 사용한다. 1번은 외부 서비스에 의존하는 꼴이라 외부 CDN에 문제가 생기면 우리 서비스도 문제가 발생하기 때문
등록화면 구현
2개의 라이브러리 부트스트랩과 제이쿼리 추가 및 index에 레이아웃 방식 적용
* 레이아웃: 공통 영역을 별도의 파일로 분리하여 원할 때 가져다 쓰는 방식 -> 중복 코드 작성 방지
header.mustache와 footer.mustache 생성 (src/main/resources/templates/laout)
header.mustache - css 위치
<!DOCTYPE HTML>
<html>
<head>
<title>스프링부트 웹서비스</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!--html은 위에서부터 코드실행-> head가 다 실행되고 body 실행-->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</head>
<body>
footer.mustache - js 위치
<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>
위에서 보이듯이
css와 js의 위치가 서로 다른 이유
HTML은 위에서부터 실행되기 때문에, head 실행 후 body가 실행된다. -> 즉, head가 실행되지 않으면 백지상태
- 페이지의 로딩 속도를 높이기 위해 css는 화면을 그리는 역할로 body보다 먼저 실행되도록 head에서 불러오고, js는 용량이 클수록 로딩이 오래걸리기 때문에 body 하단에서 호출하게 한다.
글 등록 페이지
index.mustache에 등록 버튼 추가 및 이동할 페이지에 대한 컨트롤러 IndexController에 생성
이동주소는 /posts/save이다.
위 과정의 코드는 이전에 한 과정과 비슷하고 간단하므로 생략한다.
posts-save.mustache 생성 (파일 위치는 index.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}}
글 등록화면 모습
글 등록을 클릭했을 때, API 호출하도록 JS 생성
index.js (src/main/resources/static/app)
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: '/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));
});
}
};
main.init();
type: 'POST'
-> http 메소드 중에서 POST를 선택한다.
REST에서 CRUD에 매핑되는 http 메소드
- create - POST
- read - GET
- update - PUT
- delete - DELETE
위에서 보면 var main={ ... }라는 코드로 index라는 변수의 속성으로 function을 추가했다. 이는 덮어쓰기가 되는 것을 막기 위함이다. 브라우저의 스코프(scope)는 공용 공간으로 쓰이기 때문에 나중에 로딩된 다른 js의 init, save가 먼저 로딩된 js의 function을 덮어쓰게 될 수 있다.
이를 막기 위해 var index 객체를 만들어서 해당 객체에서 필요한 모든 function을 선언해둔다. index 객체안에서만 function이 유효하도록 해서 다른 JS와 겹치는 것을 막아준다.
즉, js 객체를 만들어 사용하는 이유는
브라우저의 전역 변수 충돌문제를 회피하기 위함이다. (덮어쓰기되는 현상)
게시글 등록 기능 실행 결과
실제로 H2 DB에 등록되었는지 http://localhost:8080/h2-console/로 접속한다.
콘솔에 접속한 뒤, 테이블을 조회하면 방금 등록한 데이터가 저장된 것을 확인 할 수 있다.
전체 조회화면 만들기
index.mustache 수정
- 조회한 목록이 나오도록 수정한다.
{{>layout/header}}
<h1>스프링 부트로 시작하는 웹서비스 ver.2</h1>
<h2>게시글</h2>
<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라는 List 순회 (자바의 for문)-->
{{#posts}}
<tr>
<td>{{id}}</td>
<td>{{title}}</td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
</table>
</div>
{{>layout/footer}}
{{#posts}}: posts라는 List를 순회한다. (자바의 for문 역할)
{{id}}등 변수명: List에서 뽑아낸 객테의 필드 사용
PostRepository 인터페이스에 쿼리 추가
public interface PostsRepository extends JpaRepository<Posts, Long> {
//SpringDatatJpa 제공하지 않는 메소드는 쿼리로 작성 가능, 아래의 경우는 제공되는 기본메소드로도 가능
//쿼리는 가독성이 좋아 상황따라 선택해서 결정
@Query("select p from Posts p ORDER BY p.id DESC")
List<Posts> findAllDesc();
}
SpringDataJpa에서 제공하지 않는 메소드는 위처럼 쿼리로 작성 가능, 위에서는 제공되는 기본 메소드만으로도 해결할 수 있다. 다만 쿼리는 가독성이 훨씬 좋기 때문에 상황에 따라 선택해서 사용한다.
PostService에 코드추가
findAllDesc 메소드 작성
//readOnly = true -> 트랜잭션 범위는 유지하되, 조회기능만 남기기 -> 조회속도 개선
@Transactional(readOnly = true)
public List<PostsListResponseDto> findAllDesc() {
return postsRepository.findAllDesc().stream()
//postsRepository 결과로 넘어온 Posts의 Stream을 map을 통해 PostsListResponseDto로 변환
// -> List로 반환하는 메소드
.map(PostsListResponseDto::new)
.collect(Collectors.toList());
}
@Transactional(readOnly = true) : readOnly = true 옵션은 트랜잭션의 범위는 유지하되, 조회기능만 남긴다는 의미로 조회 속도가 개선된다. 등록,수정,삭제 기능이 필요없는 메소드라면 이를 통해 조회 속도를 개선할 수 있다.
PostsListResponseDto 생성 (com.project.web-service.web.dto)
@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();
}
}
IndexController 수정
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
@GetMapping("/")
public String index(Model model){
model.addAttribute("posts",postsService.findAllDesc());
return "index";
}
@GetMapping("/posts/save")
public String postsSave(){
return "posts-save";
}
}
Model: 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있다. 위에서는 postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache로 전달한다.
실행 결과
메인 페이지

글 등록

등록후 메인페이지

게시글 수정화면 만들기
등록과 마찬가지로 update api도 3장할 때, 작성했기 때문에 화면만 개발하면 된다.
posts-update.mustache 생성 (src.main.resources.templates)
{{>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}}: 머스테치는 객체의 필드 접근시 점(Dot)으로 구분 ex) Post 클래스의 id에 대한 접근 -> post.id
readOnly: Input 태그에 읽기 가능만 허용하는 속성, 위 코드에서는 id와 author는 수정할 수 없도록 읽기만 허용하도록 추가했다.
index.js 수정
update 기능 호출 추가
var main = {
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));
});
}
};
main.init();
index.mustache에 글 수정페이지로 이동 기능 추가
전체 목록에서 수정 페이지로 이동
title 클릭시 이동하도록 <a> 태그 추가
<tbody id="tbody">
<!--posts라는 List 순회 (자바의 for문)-->
{{#posts}}
<tr>
<td>{{id}}</td>
<td><a href="/posts/update/{{id}}">{{title}}</a></td>
<td>{{author}}</td>
<td>{{modifiedDate}}</td>
</tr>
{{/posts}}
</tbody>
IndexController 수정
a 태그에 있는 주소로 요청오면 수정 페이지로 이동
@GetMapping("/posts/update/{id}")
public String postsUpdate(@PathVariable Long id, Model model){
PostsResponseDto dto = postsService.findById(id);
model.addAttribute("post",dto);
return "posts-update";
}
프로젝트를 다시 실행 후, 글을 작성하면 제목이 클릭이 가능하도록 되어있다. 제목을 클릭하면 수정 페이지로 이동한다.
만약 수정하고 돌렸는데 안된다면 페이지를 새로고침 해보길 권한다.
크롬에서 개발자 도구를 켜고 js코드를 들어가보니 update에 관해 작성한 부분이 없었다.
수정된 코드로 업데이트 되어야 하는데, 이전에 작성하고 돌린 코드가 업데이트 되지 않아 생긴 문제다. 이럴때는 ctrl+f5로 새로고침하면 코드가 업데이트되고 수정버튼이 작동한다.
게시글 삭제 화면 만들기
posts-update.mustache 수정
수정완료 버튼이 작성된 코드 바로 밑에 아래 코드를 추가한다.
<button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
index.js 수정
delete 기능 호출 추가
type이 DELETE인 것을 제외하고는 이전에 작성한 것과 거의 비슷하다.
PostsService 수정
삭제 API 추가
PostsApiController 수정
서비스에서 만든 delete 메소드를 컨트롤러에서 사용하도록 코드를 추가한다.
삭제관련 코드는 이전에 작성한 코드와 비슷하므로 생략한다.
'스프링부트와 AWS로 혼자 구현하는 웹서비스' 카테고리의 다른 글
스프링 부트 어노테이션 기반으로 개선 및 세션 저장소로 jdbc 등록 (0) | 2023.03.06 |
---|---|
스프링 시큐리티와 OAuth2.0으로 로그인 구현하기 - 구글 로그인 (0) | 2023.03.06 |
스프링 부트 서버 템플릿 엔진과 클라이언트 템플릿 엔진 차이 (feat. 머스테치 사용법) (2) | 2023.03.01 |
스프링 부트 JPA Auditing 생성/수정시간 자동화 (0) | 2023.02.25 |
스프링 부트 Spring Data JPA 적용 및 등록,수정,조회 API 작성하기 (0) | 2023.02.24 |
댓글