스프링 게시판 만들어보기 3 - 머스태치로 화면 그리기

HiroPark·2022년 9월 11일
0

Spring

목록 보기
5/11

머스태치가 무엇인가!…

머스태치는 서버 사이드 템플릿 엔진의 일종인데, 템플릿 엔진은 지정된 템플릿 양식과 데이터를 합쳐서 HTML을 출력해주는 역할을 한다. 템플릿 엔진에는 서버사이드 템플릿 엔진과, 클라이언트 사이드 템플릿 엔진이 있다.

  • 서버 사이드 템플릿 엔진

    - 서버에서 구동
    - 서버에서 데이터를 템플릿에 넣은 HTML을 만들어 클라이언트에 전달해준다.
    - HTML코드에서 고정적인 부분은 템플릿으로 만들어두고, 변동이 있는 부분만 템플릿의 부분부분에 끼워넣는다.
    - 서버에서 소스코드를 실행하여 html을 완성한 뒤 보내주는 방식
  • 클라이언트 사이드 템플릿 엔진


- 브라우저 위에서 작동
- 서버에서 벗어나 브라우저에서 화면을 생성
- 서버에서는 Json형태의 데이터만을 전달하고, 이를 클라이언트에서 조립
- 소스코드는 브라우저에서 실행된다

  • 머스태치를 사용하기 위해서 머스태치 스타터 의존성을 build.gradle에 등록합니다

implementation('org.springframework.boot:spring-boot-starter-mustache')

Mustache 파일의 위치는 기본적으로 src/main/resources/templates입니다.

해당 위치에 있는 머스태치 파일을 스프링 부트가 자동으로 로드합니다.

index.mustache 부터 만듭니다.

{{>layout/header}}
    <h1>스프링 부트 테스트</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>
                {{#userName}} <!-- userName이 있다면 이를 노출 -->
                Logged in as : <span id="user"> {{userName}}</span>
                <a href = "/logout" class="btn btn-info active" role = "button">Logout</a>
                {{/userName}}
                {{^userName}} <!--userName이 존재하지 않는 경우 로그인버튼 -->
                    <a href ="/oauth2/authorization/google"
                       class = "btn btn-success active" role="button">Google Login</a>
                {{/userName}}
            </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}} <!--posts 리스트를 순회, {{id}} 등의 변수 는 여기서 뽑아낸 객체의 필드, 컨트롤러에서 model 객체를 통해 넘겨줌-->
                <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}}
  • {{>layout/header}} , {{>layout/footer}} : {{>}}는 현재 파일을 기준으로 다른 파일을 가져옵니다.
    • 이들은 공통 영역을 담당하는 부트스트랩과 제이쿼리를 불러옵니다.
  • 글 등록
    • 글 등록 버튼을 누르면 /posts/save 로 이동합니다. 모든 매핑은 Controller에서 담당합니다

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}}
  • <button type="button" class="btn btn-primary" id="btn-save">등록</button> : 등록 버튼에 실제 등록 기능을 추가해줘야 합니다.
    • 이를 위해 API를 호출하는 JS를 src/main/resources/static/js/app에 index.js로 생성합니다.

      var main = {
          init : function () {
              var _this = this;
              $('#btn-save').on('click', function () {
                  _this.save();
              });
      
              $('#btn-update').on('click', function () {
                  _this.update();
              });
      
              $('#btn-delete').on('click', function () {
                  _this.delete();
              });
          },
          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: 'POST', 
                  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));
              });
          },
          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));
              });
          }
      
      };
      
      main.init();
    • var main = { } 안에 모든 코드를 작성합니다

    • 브라우저의 스코프는 공용공간으로 쓰이기에, 나중에 로딩된 js의 init, save등의 함수가 먼저 로딩된 init, save를 덮어씁니다.

    • 이를 방지하기 위해 유효범위를 만드는데, index.js 만의 유효범위를 만들어 사용하기 위해, 객체를 만들어 해당 객체에서 필요한 모든 function을 선언합니다

    • $('#btn-save').on('click', function () { _this.save(); });
      - btn-save라는 id를 가진 HTML엘리먼트에 click이벤트 발생시, update function을 실행

      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> <!--필드 접근시 .(dot)으로 구분-->
                  </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> <!-- id와 author는 수정불가 -->
                  </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>
              <button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
          </div>
      </div>
      
      {{>layout/footer}}

부트스트랩/제이쿼리

공통 영역을 별도의 파일로 분리하여 필요할때 가져다 쓰는 방식을 레이아웃 방식이라 합니다.

이를 활용하여 머스태치 화면 어디서나 필요한 부트스트랩과 제이쿼리를 추가해 보겠습니다.

templates/layout에 footer.mustache와 header.mustache를 생성합니다.

  • header.mustache
<!doctype html>
<html lang="en">
<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>
  • 외부 CDN방식을 활용하여 라이브러리들을 추가하였습니다.
  • css는 헤더부분에, js는 footer에 둔 이유는 다음과 같습니다.
    • HTML은 위에서부터 아래로 코드가 실행되기에, head → body의 순서로 실행됩니다
    • 그런데 head가 다 불러지지 않으면, 사용자에겐 백지화면만이 보입니다.
    • header에 (head태그안에) js파일을 두어서 용량이 커질수록 body의 실행이 늦어지기 때문에, js를 body 하단에 두어 화면이 다 그려진 후에야 호출합니다
    • 반대로 css는 화면을 그려야 하기때문에 head에서 불러옵니다
    • 또한, boostrap.js는 제이쿼리에 의존(제이쿼리가 있어야만 함)하기에, jquery.js 다음으로 호출합니다.
  • src/main/resources/static에 위치한 js,css,image등 정적 파일은 URL에서 “ / “로 설정되기에 index.js를 불러올때 “ / “ 부터 시작합니다.

Controller

mustache파일에 URL을 매핑하는 것은 Controller에서 담당합니다.

@RequiredArgsConstructor
@Controller
public class IndexController {

    private final PostsService postsService;
    private final HttpSession httpSession;

    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute("posts", postsService.findAllDesc());
        SessionUser user = (SessionUser) httpSession.getAttribute("user");
        if (user != null) {
            model.addAttribute("userName", user.getName());
        }

        return "index"; 
    }
		
		@GetMapping("/posts/save")
    public String PostsSave() {
        return "posts-save";
    }
			
		@GetMapping("/posts/update/{id}")
    public String postsUpdate(@PathVariable Long id, Model model) {
        PostsResponseDto dto = postsService.findById(id);
        model.addAttribute("post", dto);

        return "posts-update";
    }
}
  • 머스태치 스타터가 “index” 앞의 경로를 src/main/resources/templates로, 뒤의 확장자를 .mustache 로 자동 지정합니다.
  • 즉 src/main/resources/templates/index.mustache 로 전환되어, URL요청의 결과를 전달한 타입과 값을 지정하는 관리자인 View Resolver가 이를 처리합니다.
  • SessionUser는 소셜로그인에 관련된 설정입니다
    • 로그인 성공시 세션에 SessionUser를 저장합니다.
    • 세션에 저장된 값이 있을때에만 모델에 userName으로 등록합니다.
    • 세션에 저장된 값이 없으면 머스태치 문법에 따라 — {{^userName}} <!--userName이 존재하지 않는 경우 로그인버튼 --> — 로그인 버튼이 보입니다

Service

  • PostsService
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

  
    @Transactional(readOnly = true) //트랜젝션 범위는 유지하나, 조회 기능만 남겨서 조회 속도를 개선함
    public List<PostsListResponseDto> findAllDesc() {
        return postsRepository.findAllDesc().stream()
                .map(posts -> new PostsListResponseDto(posts)) // Posts의 스트림을 map을 통해 dto의 리스트로 반환
                .collect(Collectors.toList());
    }
}
  • PostsRepository의 findAllDesc()
@Query("SELECT p FROM Posts p ORDER BY p.id DESC")
    List<Posts> findAllDesc();
profile
https://de-vlog.tistory.com/ 이사중입니다

0개의 댓글