ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [4장] 머스테치로 화면 구성하기
    Spring/스프링 부트와 AWS로 혼자 구현하는 웹 서비스 2022. 11. 21. 22:10

     

    ✏️ 서버 템플릿 엔진과 메스테치 소개

     

    • JSP를 비록한 서버 템플릿 엔진은 서버에서 구동됨
    • 서버 템플릿 엔진을 이용한 화면 생성
      • 서버에서 Java 코드로 문자열을 만든 뒤 이 문자열을 HTML로 변환하여 브라우저로 전달
    • 브라우저에서 자바스크립트가 작동될 때는 서버 템플릿 엔진의 손을 벗어나 제어할 수 없게 됨

     

    📎머스테치

     

    • 장점
      • 문법이 다른 템플릿 엔지보다 간단함
      • 로직 코드를 사용할 수 없어 View 의 역할과 서버의 역할이 명확하게 분리됨
      • 현존하는 대부분의 언어를 지원하기 때문에 서버와 템플릿 엔진으로 모두 사용 가능

     

    머스테치 의존성 추가

     

    IndexController

    @RequiredArgsConstructor
    @Controller
    public class IndexController {
    
        private final PostsService postsService;
    
        @GetMapping("/")
        public String index(Model model) { //Model: 서버 템플릿 엔진에서 사용할 수 있는 객체 저장
            model.addAttribute("posts", postsService.findAllDesc());
            return "index";
        }
    
        @GetMapping("/posts/update/{id}")
        public String postsUpdate(@PathVariable Long id, Model model) {
            PostsResponseDto dto = postsService.findById(id);
            model.addAttribute("post",dto);
            return "posts-update";
        }
    //    @GetMapping("/")
    //    public String index() {
    //        return "index"; //머스테치에서 문자열 반환 시 앞의 경로와 뒤의 파일 확장자 자동으로 지정된다
    //    }
    
        @GetMapping("/posts/save")
        public String postsSave() {
            return "posts-save";
        }
    }
    • 머스테치 스타터는 컨트롤러가 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정

     

    IndexController

    @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);
    
            // URL 호출 시 페이지 내용 제대로 호출 되었는지 테스트 -> 코드가 잘 포함되어 있는지 확인하면 된다.
            assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
        }
    }
    • 실제 URL호출 시 페이지의 내용이 제대로 호출되는지에 대한 테스트
    • TestRestTemplate를 통해 "/"로 호출했을 때 index.mustache에 포함된 코드들이 있는지 확인

     

     

    ✏️ 게시글 등록 화면 만들기

     📌 프론트엔드 라이브러리 사용 방법

    • 외부 CDN 사용 ☝️
    • 직접 라이브러리를 받아서 사용

     

    레이아웃 방식: 공통 영역을 별도의 파일로 분리하여 필요한 곳에 가져다 쓰는 방식

     

     

    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>
    • header, footer 따로 만들어서 로딩 속도 향상
    • HTML은 위에서부터 코드가 실행되기 때문에 head가 다 실행되고서야 body가 실행됨
      • 따라서 body를 하단에 두어야 효율적

     

    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>
    
        <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}}</td>
                    <td>{{author}}</td>
                    <td>{{modifiedDate}}</td>
                </tr>
            {{/posts}}
            </tbody>
        </table>
    </div>
    
    
    {{>layout/footer}}

     

    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}}

     

     

    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: '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));
            });
        },
        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();

     

    • $('#btn-update').on('click')
      • btn-update란 id를 가진 HTML 엘리먼트에 click 이벤트가 발생할 때 update function을 실행하도록 이벤트를 등록
    • update : function ()
      • 신규로 추가될 update function
    • type:'PUT'
      • 여러 HTTP Method 중 PUT메소드를 선택
      • REST에서 CRUD는 아래와 같이 HTTP Method에 매핑됨
        • Create - POST
        • Read - GET
        • Update - PUT
        • Delete - DELETE

     

     

    상황: index.mustache에서 a.js가 추가되어 a.js도 a.js만의 init과 save function이 있다면?

    • 브라우저의 스코프는 공용공간으로 쓰임
      • => 나중에 로딩된 js의 init, save가 먼저 로딩된 js의 function을 덮어쓰게 됨
    • 해결법
      • var.index라는 객체를 만들어 해당 객체에서 필요한 모든 function을 선언
        • index 객체 안에서만 function이 유효하기 때문에 다른 JS와 겹칠 위험이 사라짐

     

    머스테치 문법

    • <<#posts>>
      • posts라는 List를 순회
      • Java의 for문과 동일하게 생각!
    • <<id>>등의 {{변수명}}
      • List에서 뽑아낸 객체의 필드를 사용

     

    PostsRepository

    public interface PostsRepository extends JpaRepository<Posts,Long> {
    
        @Query("Select p from Posts p order by p.id DESC ")
        List<Posts> findAllDesc();
    }
    • SpringDataJpa에서 제공하지 않는 메소드는 쿼리로 작성해도 됨!

     

     

    PostsListResponseDto

    @Getter
    public class PostsListResponseDto {
        private Long id;
        private String title;
        private String content;
        private String author;
        private LocalDateTime modifiedDate;
    
        public PostsListResponseDto(Posts entity) {
            this.id = entity.getId();
            this.title = entity.getTitle();
            this.content = entity.getContent();
            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: 서버 템플릿 엔진에서 사용할 수 있는 객체 저장
            model.addAttribute("posts", postsService.findAllDesc());
            return "index";
        }
    • Model
      • 서버 템플릿 엔진에서 사용할 수 있는 객체를 저장할 수 있음
      • postsService.findAllDesc()로 가져온 결과를 posts로 index.mustache에 전달하는 역할

     

     

    ✏️ 게시글 수정, 삭제 화면 만들기

     

    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>
            <button type="button" class="btn btn-danger" id="btn-delete">삭제</button>
        </div>
    </div>
    
    {{>layout/footer}}
    • {{posts.id}}
      • 머스테치는 객체의 필드 접근 시 점으로 구분
      • Posts클래스의 id에 대한 접근은 post.id로 사용 가능
    • readonly
      • input 태그에 읽기 가능만 허용하는 속성
      • id와 author는 수정할 수 없도록 읽기만 허용하도록 추가

     

     

     

    IndexController 추가

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

     

    PostsApiController 추가

    @DeleteMapping("/api/v1/posts/{id}")
    public Long delete(@PathVariable Long id) {
        postsService.delete(id);
        return id;
    }

     

     

Designed by Tistory.