[Spring] 스프링 부트와 AWS로 혼자 구현하는 웹 서비스4

김나윤·2024년 10월 8일
0

Spring

목록 보기
6/9

04. 머스테치로 화면 구성하기

1) 서버 템플릿 엔진과 머스테치 소개

템플릿 엔진

일반적으로 웹 개발에서 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어를 의미한다.
쉽게 말하면 웹 사이트의 화면을 어떤 형태로 만들지 도와주는 양식이다.
JSP, Freemarker, React, View 같은 것들이 대표적인 예다.

머스테치?

JSP와 같이 HTML을 만들어 주는 템플릿 엔진이다.
루비, 자바스크립트, 파이썬, PHP, 자바, 펄, Go, ASP 등 현존하는 대부분의 언어를 지원하고 있다.

머스테치 플러그인 설치

Shift 두 번 누르기 > Plugins 검색 > Marketplace 에서 mustache 검색 > Install

설치가 완료되면 인텔리제이를 재시작해 플러그인이 작동되는지 확인하면 된다.


2) 기본 페이지 만들기

먼저 스프링 부트 프로젝트에서 머스테치를 편하게 사용할 수 있도록 머스테치 스타터 의존성을 build.gradle에 등록한다.

머스테치는 스프링 부트에서 공식 지원하는 템플릿 엔진으로, 읜존성 하나만 추가하면 다른 스타터 패키지와 마찬가지로 추가 설정 없이 설치가 끝난다.
별도로 스프링 부트 버전을 개발자가 신경 쓰지 않아도 되는 장점이 있다.

머스테치 파일 위치는 기본적으로 src/main/resources/templates 로, 이 위치에 머스테치 파일을 두면 스피링 부트에서 자동으로 로딩한다.

그럼 첫 페이지를 담당할 index.mustache 를 생성해보자.

src/main/resources/templates 경로에 index.mustache 파일을 생성한다.

간단하게 "스프링 부트로 시작하는 웹 서비스"를 출력하는 페이지를 작성한다.

그 후, src/main/~/java/web 경로에 IndexController를 생성해,
Controller에서 위에 작성한 머스테치에 URL을 매핑한다.

여기까지 코드를 작성했으면 테스트 코드로 프로그램이 정상적으로 동작하는지 검증해보자.

test/java/~/web 경로에 IndexControllerTest.java 파일을 만들어 주고,

위와 같이 테스트 코드를 입력했다.
두근두근 Run time~

ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ

왜 오류가 뜨는지 찾아봤는데,

(참고)
https://yeoonjae.tistory.com/entry/Spring-WebMvcTest-%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%8B%9C-Bean-%EC%A3%BC%EC%9E%85-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0

어노테이션을 제대로 명시해주지 않아서라고 생각되었다.
코드를 다시 한 번 확인해보니 Controller 뿐만 아니라 ControllerTest에서도 각각 @Controller@ExtendWith & @SpringBootTest 를 써주지 않았던 걸 발견했다.

어노테이션을 적어주고 다시 돌려보았지만 여전히 문제가 해결되지 않았다.
흠.. 뭐가 문젤까
뭔가 HTML 코드가 찍히는 걸로 봐선 조금만 고치면 될 것 같은데...

계속 고민하다,

(참고)
https://github.com/jojoldu/freelec-springboot2-webservice/issues/649

이 깃헙 댓글을 보고 경로나 파일 확장자가 잘못 되었나 싶었다.

그러다 index라는 이름을 가질 파일이 두 개가 있는 것을 발견했다.
static 폴더 안에 있는 index.html은 앞선 오류를 해결하기 위해 넣어준 파일이었는데 혹시 같은 이름을 가진 파일이 2개가 있어 어떤 파일을 지정해야 할 지 해결되지 않아 오류가 뜨나 싶었다.

하지만 다시 생각해보니 이건 상관 없는 문제 같았다.
왜냐면 앞서 mustache 플러그인을 설치했기 때문에 머스테치 스타터가 컨트롤러에서 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정해준다고 알고 있었다.
따라서 앞의 경로는 자동으로 src/main/resources/templates이 될 거고,
뒤의 확장자로는 .mustache가 붙을 거니까 아무리 봐도 상관 없다..
실제로 index.html 파일을 삭제해봐도 역시 문제는 여전했다.

build.gradle 설정 문제인가 싶었는데 해당 mustache 설정을 제거해도 해결되지 않았다...

(참고)
https://suucong.tistory.com/29

그러던 중 발견한 한줄기의 빛 같은 글!
나랑 똑같은 문제를 해결하신 분을 찾았다.
이 분처럼 나 역시 스프링 부트로 시작하는 웹 서비스 라는 글자가 깨져서 ??? 로 표시되었는데, 알려주신 것처럼 application.properties에 관련 설정을 추가해주니 해결되었다.

이 코드는 HTTP 응답의 문자 인코딩을 강제로 설정하는 옵션이라고 한다. true로 설정하면 Spring Boot 애플리케이션의 모든 HTTP 응답의 캐릭터 인코딩을 지정한 캐릭터셋으로 강제줘서, 위의 mustache 코드에서 charset을 UTF-8로 해주었는데 안먹혔던걸 강제로 적용시킨다고 한다.

그런데 테스트가 성공했다고 뜨기는 하는데 Run을 하면 무한로딩에 걸리는 거..
얜 또 왜 이런 거지 싶었는데, 톰캣에 연결이 안 된 건가 싶었다.
application 클래스에서 main메소드를 실행하고 테스트 코드를 다시 돌렸더니 무한로딩에서 벗어나 정상적으로 테스트가 완료 되었다.

햐 드디어 테스트 성공ㅠㅠ

http://localhost:8080 으로 들어가보니 내가 입력한 코드가 실제로 구현되어 있는 것을 확인할 수 있었다.


3) 게시글 등록 화면 만들기

앞선 3장의 PostApiController로 API를 구현하는 방법과 달리, 이번엔 바로 화면을 개발해보았다.
이때, 부트스트랩을 이용해 화면을 만들었다.

프론트엔드 라이브러리(부트스트랩, 제이쿼리 등..)를 사용하는 방법

  • 외부 CDN 사용
  • 직접 라이브러리 다운받기

프론트엔드 라이브러리를 사용하는 방법으로 외부 CDN을 사용했다.
본인의 프로젝트에서 직접 내려받아 사용할 필요도 없고, 사용 방법도 HTML/JSP/Mustache에 코드만 한 줄 추가하면 되어 간단하다.

이걸 보고 '국비 다닐 때는 직접 라이브러리를 다운 받아 사용했던 것 같은데? 이렇게 간단한 방법이 있는데 왜 굳이 라이브러리를 다운받아 사용했던 걸까?' 하는 의문이 들었다.

책에 추가로 설명된 내용으로 이 의문을 해결할 수 있었는데,
실제 서비스에서는 외부 CDN을 잘 사용하지 않는다고 한다. 결국은 외부 서비스에 우리 서비스가 의존하게 돼버려서, CDN을 서비스하는 곳에 문제가 생기면 덩달아 같이 문제가 생기기 때문이라고 한다.
역시 다 이유가 있군...칫...

그렇다면 2개의 라이브러리 부트스트랩과 제이쿼리를 index.mustache에 추가해야 한다. 하지만 바로 추가하지 않고 레이아웃 방식으로 추가했다.

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

이번에 추가할 라이브러리들인 부트스트랩과 제이쿼리는 머스테치 화면 어디서나 필요하기 때문에 매번 해당 라이브러리를 머스테치 파일에 추가하는 것은 번거로운 일이기 때문에 레이아웃 파일들로 만든다.

src/main/resources/templates 디렉토리에 layout 디렉토리를 추가했다.
그리고 footer.mustache, header.mustache 파일을 생성한다.

이렇게 틀을 만들었다면 레이아웃 파일들에 각각 공통 코드를 추가한다.

▲ header.mustache 코드

▲ footer.mustache 코드

여기서 짚고 넘어가야 할 부분이 있다.
바로 css와 js의 위치이다. css는 header에, js는 footer에 두었는데 이것 역시 이유가 있다.

Q. 왜 css는 header에, js는 footer에 위치시킬까?


HTML은 위에서부터 코드가 순차적으로 진행되기 때문에 header가 다 실행되고서야 body가 실행된다.
즉, header가 다 불러지지 않으면 사용자 쪽에선 백지 화면만 노출된다. 특히 js의 용량이 크면 클수록 body 부분의 실행이 늦어지기 때문에 js는 body 하단에 두어 화면이 다 그려진 뒤에 호출하는 것이 좋다.


반면에 css는 화면을 그리는 역할이므로 head에서 불러오는 편이 좋다.
그렇지 않으면 css가 적용되지 않은 깨진 화면을 사용자가 볼 수 있기 때문이다.

추가로 bootstrap.js의 경우, 제이쿼리가 꼭 있어야만 하기 때문에 부트스트랩보다 먼저 호출되도록 코드를 작성했다. (bootstrap.js가 제이쿼리에 의존)

이렇게 라이브러리를 비롯해 기타 HTML 태그들이 모두 레이아웃에 추가되니, index.mustache에는 필요한 코드만 남게 된다.

이랬던 index.mustache 코드를,

이렇게 간단하게 정리할 수 있다.
이처럼 레이아웃으로 파일을 분리한 후, index.mustache에 글 등록 버튼을 하나 추가한다.

<a> 태그를 이용해 글 등록 페이지로 이동하는 글 등록 버튼을 만들었다. 이때 이동할 페이지 주소는 /posts/save 이다.
=> 그럼 이제 /posts/save 에 해당하는 컨트롤러를 생성한다. (페이지에 관련된 컨트롤러는 모두 IndexController를 사용한다.)

src/main/java/com/springboot/project/springboot_webservice_project/web/IndexController.java 디렉토리에 컨트롤러를 생성한다.
컨트롤러 코드가 생성되었다면 posts-save.mustache 파일을 생성한다.
파일의 위치는 index.mustache와 같다.

UI가 완성되었으니 다시 프로젝트를 실행하고 브라우저에서 http://localhost:8080/ 으로 접근해본다.

ㅋㅋㅋㅋㅋㅋ큐ㅠㅠㅠㅠㅠㅠ접근 못 했다ㅠㅠㅠㅠㅠㅠ
설상가상으로 잘 뜨던 index 역시 뜨지 않았다.

가장 처음 생각된 문제 후보는 "경로를 잘못 입력했나? 오타 냈나?"였다.
아닌데... 코드 잘 적었는데..
해당 오류를 구글링 해보니 여러 해결책이 나왔지만 나와는 해당 사항이 없는 것 같았다.

인텔리제이에서 천천히 오류 내용을 확인해보기로 했다.
반복적으로 발생한 오류가 있었는데 바로,

java.io.FileNotFoundException: class path resource [templates/.mustache] cannot be opened because it does not exist 이 오류였다.

뭔가 경로 설정이 잘못됐나 싶은데..

오류 찾다가 footer 부분을 깃허브에서 받아왔더니 index.js 추가 부분까지 들고 온 걸 보고 일단 지우고..

index.mustache 파일에서 a태그로 /posts/save 경로를 지정한 부분에 밑줄이 있길래 봤더니 Cannot resolve directory 'post' 라는 경고문이 떠있었다.
얘가 문제인 건가..? 내가 경로를 이상하게 지정한 건가 싶어서 경로와 mustache 확장자명에 오타를 낸 건 아닌지 유심히 봤지만 이것도 답이 아니었다.

이것저것 구글링하며 해결법을 찾아봤는데 내 문제를 해결하지는 못 했다.
결국 chatGPT에 오류 내용과 코드를 입력하고 물어봤더니 여러가지 문제 후보들을 제시해주었다.

5번 해결책을 보다 보니 index.mustache 에서 {{>}} 에 대한 설명을 주석으로 써두었는데 빨간 밑줄이 생겼던 부분이 걸렸다.
그래서 그 주석을 삭제하고 다시 돌려보았더니....

▲ 문제의 주석..

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ큐ㅠㅠㅠㅠㅠㅠ
해결했는데 왜 이렇게 슬프지..

힝ㅋㅋㅋㅋㅋ그래도 다행이다..

찾아보니 <!-- --> 는 HTML 주석이고 머스테치는 이를 잘 인식하지 못 할 수 있다고 한다. 그래서 주석을 머스테치 문법인 {{! your comment}} 로 바꿔주고 다시 실행해보았더니 문제없이 주석처리가 되는 것을 확인했다.

글 등록 버튼을 눌러보니 /posts/save 경로로 잘 연결된 것을 확인할 수 있다. 취소 버튼을 누르면 index 화면으로 잘 돌아가는 것 역시 확인했다.

하지만 아직 API를 호출하는 JS가 없기 때문에 게시글 등록 버튼은 기능이 없다. 그래서 src/main/resources에 static/js/app 디렉토리를 생성한다.

그리고 이 디렉토리에 index.js 파일을 생성한다.

▲ index.js 코드

이렇게 코드를 입력하고 생성한 index.js를 머스테치 파일이 쓸 수 있게 footer.mustache에 추가한다.

모든 코드를 완성한 뒤, 등록 기능을 브라우저에서 테스트 해보았다.

등록 버튼을 눌렀지만 alert가 뜨지 않았고 코드를 잘못 적었나 싶어서 확인해보니

index.js 코드에서 var _this = this; 부분을 var_this = this; 라고 적어 var이 변수로 제대로 인식되지 않은 것을 발견했다.
또한, #btn-save 부분에서 #을 제외하고 입력한 것을 발견해 수정해주었다.

그럼에도 불구하고 여전히 해결되지 않았다.
인텔리제이에서 따로 디버깅한 내용을 보여주지 않아 크롬의 개발자 도구를 사용했더니,

Uncaught ReferenceError: index is not defined at index.js:31:1 란 오류 메시지를 확인할 수 있었다.

index가 정의가 안 되었다고...?
크롬이 알려준대로 index.js 파일의 31행 코드를 봤더니
index.init(); 을 가리키고 있었다.
뭐가 문제일지 고민을 해보았는데 처음에 var main 으로 변수를 선언했는데 변수명을 index로 잘못 적어 index 변수를 찾지 못해 발생하는 오류 같았다.

이를 main 으로 변경하고 다시 톰캣을 실행해보았더니,

정상적으로 글이 등록된 것을 확인할 수 있었다.


확인 버튼을 누르니 index 화면으로 재연결되었다.

추가로 localhost:8080/h2-console 에 접속해 실제로 DB에 데이터가 등록되어있는지도 확인했다.


4) 전체 조회 화면 만들기

이렇게 등록 기능이 정상적으로 작동하는 것을 확인하고 이어서 전체 조회 화면을 만들어 보았다.

<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>
            <!-- 이동할 페이지 주소는 /posts/save-->
        </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>{{title}}</td>
                <td>{{author}}</td>
                <td>{{modifiedDate}}</td>
            </tr>
        {{/posts}}
        </tbody>
    </table>

</div>

먼저, 전체 조회를 위해 index.mustache의 UI를 변경한다.
이때, 머스테치 문법이 사용되는데 머스테치에 대한 자세한 내용은 다른 포스트로 정리해두었다.
간단히 정리하자면 {{#posts}} 는 posts 라는 List를 순회한다. Java의 for문과 동일하게 생각하면 된다길래 이해하는 데 어렵지 않았다.
{{id}} 와 같은 형식은 변수명을 나타내는데, List에서 뽑아낸 객체의 필드를 사용한다고 한다.

다음으로 Controller, Service, Repository 코드를 작성한다.

▲ PostsRepository.java

▲ PostsService.java

▲ PostsListResponseDto.java

▲ IndexController.java

이렇게 각각의 코드를 수정해주고 PostsService에서 @Transactional(readOnly = true) 부분에서 readOnly가 제대로 import 되지 않은 듯했다.

확인해보니 Transactional를 import 할 때 org.springframework가 아닌 jakarta를 써둔 것을 발견했다.

해당 import를 삭제하고 다시 제대로 import 해주었더니 문제가 해결되었다.

돌아온 두근두근 확인시간..
메인 화면은 정상적으로 떴지만..

글 등록을 누르니 Whitelabel 페이지가 떴다.
마찬가지로 콘솔을 확인해보니 /post/save 부분에 문제가 생긴 듯했다.
다시 코드로 돌아가 해당 부분을 확인해보니, /post/save 에 매핑한 코드를 주석처리해서 인식되지 않아 그런 것이었다.

주석을 해제하고 재실행하니 정상적으로 게시글 목록 기능이 작동하는 것을 확인할 수 있었다.


5) 게시글 수정, 삭제 화면 만들기

마지막으로 게시글 수정, 삭제 화면을 만들어 보았다.

- 게시글 수정

먼저 게시글 수정 화면을 만들기 위해 머스테치 파일을 생성한다.

▲ posts-update.mustache

▲ index.js

▲ index.mustache

▲ IndexController.java

글 번호와 작성자는 수정되지 않는 것을 확인하고 제목과 내용을 변경해 수정완료를 눌러보았지만 작동하지 않았다.
콘솔창에 오류 메시지가 뜨는 것도 없고 나머지 기능은 잘 되는데 수정 완료 버튼만 안 눌리는 것 같아 index.js에서 update 기능을 추가할 때 뭔가 실수를 한 것 같았다.
아니나 다를까 수정 완료 버튼을 눌렀을 때 update 기능이 동작하도록 하는 코드를 추가하지 않았던 것.

btn update를 클릭할 때 동작할 수 있도록 코드를 추가해주고 다시 실행해보니,

새로운 오류를 얻었다!!!!!!!!!!!!!!!

이번엔 PUT 메서드 사용이 잘못된 것 같은데 책에서는 PostsApiController에 있는 API에서 이미 @PutMapping 으로 선언했기 때문에 PUT을 사용해야 한다고 했다.
그래서 PostsApiController 로 돌아가 코드를 천천히 살펴보았다.

오잉! POST로 해둔 걸 발견했다.
@PostMappingPutMapping 으로 바꿔준 후 다시 재실행을 해보았더니,

문제없이 게시글이 수정되었다는 알림이 뜨고,

성공적으로 게시글이 수정된 것을 확인할 수 있었다.


- 게시글 삭제

삭제 버튼은 본문을 확인하고 진행해야 하므로, 수정 화면에 추가한다.

먼저, posts-update.mustache 에서 삭제 버튼을 추가해준다.

▲ index.js

이후, 삭제 이벤트를 진행할 JS 코드도 추가한다.

다음으로는 삭제 API를 만드는 단계이다.
PostsService 에서 delete 메소드를 만들고 이를 컨트롤러가 사용할 수 있도록 PostsApiController 코드를 수정한다.

헤헿 안녕 오류

코드를 잘못 적은 부분이 있나 싶어 살펴보다, $({...}) 이 부분 바로 뒤에 세미콜론을 찍지 않은 것이 보였다. 이것 때문인가 싶어 세미콜론을 찍고 다시 실행해봤지만 해결되지 않았다.

가장 의심이 가는 부분은 index.js에서 delete 기능을 추가한 코드인데 인텔리제이에서 볼 때도 save, update와 달리 delete 문자 자체가 빨간색으로 제대로 인식이 되지 않은 듯했다.

?설마 저 중괄호 뒤에 쉼표를 안 찍어서 뒷 문장이 인식이 제대로 안 된 건가..?

진짜네...
진짜 바보 같은 나....

오류를 해결하고 게시글 등록, 수정 모두 정상적으로 수행되는 것을 확인한 후,
수정한 게시글을 삭제하는데까지 성공했다.

업로드중..

메인 페이지로 돌아가니 기존 게시글이 삭제되어있는 걸 볼 수 있다.

profile
Hello, world!

0개의 댓글