1. 계기

로그인 api만 개발했을 때는 로그인 성공 후 응답에 bearer token을 발급하는 것에서 그쳤지만, 화면까지 같이 개발하면서 로그인 유저와 로그아웃 유저를 식별해야하기에 발급된 토큰을 화면에서 다루는 법을 학습하기 위해 진행했다. 

 

Spring Security로 로그인을 구현했던터라 세션 방식의 로그인을 구현할 수 있었지만, 실무에서는 세션 로그인보다는 api/토큰으로 프론트와 통신하는 작업이 더 많을 것이라 생각해 실무를 위해서 조금 어렵더라도 Spring Security, JWT 토큰, Cookie로 로그인을 구현했다.

 

2. Spring Security로 JWT 토큰 발급 및 검증 구현

이 과정은 설명이 꽤 길어서 별도의 포스트에 기록했다. 아래 링크를 참고하면 된다.

https://heyazoo1007.tistory.com/811

 

3. 로그인 구현

AuthController, signIn()

@Controller
@RequestMapping("/v1/auth")
@RequiredArgsConstructor
public class AuthController {
    private static final int LOGIN_DURATION = 60 *  60 * 10;
    private final AuthService authService;

    ...
    
    @PostMapping("/sign-in")
    @ResponseBody
    public ApiResponse<LinkResponseDto> signIn(
            @Valid @RequestBody LoginRequestDto request, HttpServletResponse response
    ) {
        TokenResponseDto tokenResponseDto = authService.signIn(request.getEmail(), request.getPassword());

        Cookie cookie = new Cookie("access_token", tokenResponseDto.getAccessToken());
        cookie.setMaxAge(LOGIN_DURATION);
        cookie.setPath("/");

        response.addCookie(cookie);

        return ApiResponse.SUCCESS;
    }
}

 

AuthService, signIn()

@Service
@RequiredArgsConstructor
public class AuthService {    
    
    public TokenResponseDto signIn(String email, String password) {
        // 해당 사용자가 존재하지 않는 경우
        Member member = notExistedMember(email);

        // 이메일에 비밀번호가 매치되지 않는 경우
        // 비밀번호가 틀렸는데 아이디 혹은 비밀번호로 출력하는 이유는 혹시 모를 개인 정보 유출 때문
        emailOrPasswordMismatch(password, member.getPassword());

        List<String> roles = new ArrayList<>();
        roles.add(member.getRole().toString());

        return jwtTokenProvider.createToken(email, roles);
    }
}

 

TokenResponseDto

@Builder
@Data
@AllArgsConstructor
public class TokenResponseDto {

    private String accessToken;
}

 

4. 토큰으로 로그인한 유저 확인

위 2, 3의 단계를 설정 완료한 뒤 브라우저에서 로그인을 성공하면 다음과 같이 HTTP 메시지 헤더 Cookie에 JWT access token 값이 들어있는 것을 확인할 수 있다.

 

 

클라이언트에서 보낸 HTTP 메시지 헤더의 Cookie값을 서버에서 유효성 검증을 하기 위해서는 JwtTokenProvider 클래스resolveToken()가 필요한데, 코드는 다음과 같다.

    public String resolveToken(HttpServletRequest request) {
        String cookie = request.getHeader("Cookie");
        if (cookie != null) {
            String bearerToken = cookie.split("access_token=")[1];
            if (StringUtils.hasText(bearerToken)) {
                return bearerToken;
            }
        }
        return null;
    }

 

위의 resolveToken()를 JwtAuthenticationFilter 클래스에서 사용하면 클라이언트에서 넘어온 Cookie에 있는 JWT토큰의 유효성을 검증할 수 있다. 

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain
    ) throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);


        // 2. validateToken 으로 토큰 유효성 검사
        ... (코드 생략)
    }
}

 

5. Cookie로 로그인 유저 식별하기

이 방법은 브라우저에서 Cookie의 값을 가져온 뒤, 서버에서 발급한 access token이 있다면 로그인을 한 것으로 보고 access token이 없다면 로그아웃된 상태로 보는 방법이다. 코드는 다음과 같다.

 

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="layout/default_layout">
<th:block layout:fragment="content">
    <script>
        $(document).ready(function() {
            initIndex();
        });
    </script>

    <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container-fluid">
            <h1 class="navbar-brand">AccountBook</h1>
            <div>
                <!--로그인 전-->
                
                ...(코드 생략)
                
                <!--로그인 후-->
                
                ...(코드 생략)
                
            </div>
        </div>
    </nav>

</th:block>
</html>

 

initIndex()

function initIndex() {
    // httpOnly 로 설정된 Cookie 는 document.cookie 로 읽을 수 없음
    const accessToken = document.cookie.split('access_token=')[1];
	
    ... (코드 생략)
    
    if(accessToken != null) { // 쿠키에 access_token 이 있을 때(로그인 했을 때)
        ... (코드 생략)   
    } else { // 쿠키에 access_token 이 없을 때
        ...(코드 생략)
    }
}

 

사실 위 코드는 cookie에 대한 보안이 없어서 토큰을 브라우저에서 바로 확인할 수 있고, 그렇기에 외부에서 토큰을 탈취하기가 쉬워 보안적으로 좋은 방법은 아니다. 쿠키의 보안을 위해 httpOnly Cookie를 사용할 수도 있지만, 프론트 프레임워크를 사용하지 않는 지금으로썬 브라우저에서 토큰을 확인할 방법이 없기 때문에(httpOnly cookie를 사용하면 document.cookie로 쿠키 값 확인이 안된다) 보안적으로 취약하지만 이 방법이 현재의 최선책이라고 생각해 진행했다. 추후에 관련된 지식이 추가되면 방법을 바꿀 예정이다.

 

 

참고

Security+타임리프+jwt를 이용한 회원가입 및 로그인

 

1. 계기

뱅크샐러드를 모티브로 시작한 프로젝트라 뱅크샐러드처럼 달력 형태로 daily 지출내역을 관리할 수 있으면 좋을 것 같다고 생각했다. 그래서 일단 달마다 시작일과 요일, 전체 날짜가 다르니 동적으로 만들어야할 것 같은데 잘 몰라서 역시 구글링을 했다. javascript로 동적인 날짜 정보를 가져오고, 그 정보를 우리가 아는 달력의 형태처럼 만들기 위해 css를 사용했다. 

 

* '예산 알림 받기' 버튼은 해당 포스트와는 관련 없는 내용이므로 설명을 하지 않았다.*

 

2. 기본 구조 - HTML

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="layout/default_layout">
<th:block layout:fragment="content">

<div id="top" class="container text-center">
    <div class="row">
        <div class="col-2"><a href="/index">AccountBook</a></div>
        <div class="col-8">My AccountBook</div>
        <div class="col-2">마이 페이지</div>
    </div>
</div>

<div id="middle" class="container text-center" style="padding-top:50px;">
    <div class="row">
        <div class="col align-self-start">
            <div class="row">
                <div class="col-auto"><button class="btn btn-light nav-btn-prev" onclick="goPrev();"><</button></div>
                <div id="year-month" class="col-auto"></div>
                <div class="col-auto"><button class="btn btn-light nav-btn-next" onclick="goNext();">></button></div>
            </div>
        </div>
            <div class="col align-self-end"><button type="button" class="btn btn-success" id="budgetToastBtn" onclick="getAlarm();">예산 알림 받기</button></div>
    </div>
</div>

<div class="calendar">
    <div class="days">
        <div class="day">MON</div>
        <div class="day">TUE</div>
        <div class="day">WED</div>
        <div class="day">THU</div>
        <div class="day">FRI</div>
        <div class="day">SAT</div>
        <div class="day">SUN</div>
    </div>
    <div class="dates"></div>
</div>

</th:block>
</html>

 

2-1. 코드 톺아보기

- 레이아웃 코드

thymeleaf로 레이아웃을 만들어 적용했기에 윗부분에 다음과 같은 코드가 있다. 이는 레이아웃을 적용하지 않았다면 필요없다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="layout/default_layout">
<th:block layout:fragment="content">

 

- 달력 넘기는 코드

<div id="middle" class="container text-center" style="padding-top:50px;">
    <div class="row">
        <div class="col align-self-start">
            <div class="row">
                <div class="col-auto"><button class="btn btn-light nav-btn-prev" onclick="goPrev();"><</button></div>
                <div id="year-month" class="col-auto"></div>
                <div class="col-auto"><button class="btn btn-light nav-btn-next" onclick="goNext();">></button></div>
            </div>
        </div>
            <div class="col align-self-end"><button type="button" class="btn btn-success" id="budgetToastBtn" onclick="getAlarm();">예산 알림 받기</button></div>
    </div>
</div>

왼쪽 상단에 현재 날짜를 보여주고, 이전달과 다음달로 이동할 수 있는 코드이고, goPrev(), goNext()로 동작한다.

 

- 캘린더 코드

<div class="calendar">
    <div class="days">
        <div class="day">MON</div>
        <div class="day">TUE</div>
        <div class="day">WED</div>
        <div class="day">THU</div>
        <div class="day">FRI</div>
        <div class="day">SAT</div>
        <div class="day">SUN</div>
    </div>
    <div class="dates"></div>
</div>

기본 캘린더 틀이고, 요일을 div로 만들고, 각 날짜를 아래 date 클래스의 div 태그에 동적으로 할당할 예정이다.

 

 

3. Javascript로 날짜 정보 동적으로 가져오기

 

3-1. Javascript 전체 코드

<script>
    var currentYear = ''; // 현재 페이지에 있는 연도
    var currentMonth = ''; // 현재 페이지에 있는 월
    var currentDate = ''; // 현재 페이지에 있는 일
    $(document).ready(function() {
        // 한국 표준시간 가져오기
        var date = new Date();
        var utc = date.getTime() + (date.getTimezoneOffset() * 60 * 1000);
        var kstGap = 9 * 60 * 60 * 1000;
        var today = new Date(utc + kstGap);
        var koreaDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());

        // 연, 월, 일 가져오기
        currentYear = koreaDate.getFullYear();
        currentMonth = koreaDate.getMonth() + 1;
        currentDate = koreaDate.getDate();

        // 페이지 로딩 초기에 달력 가져오기
        renderCalendar(currentYear, currentMonth, currentDate);
    });

    function renderCalendar(currentYear, currentMonth, currentDate) {
    	// zeroFormat()은 따로 정의한 함수, 2024-5를 2024-05로 만들어주는 역할
        $('#year-month').text(currentYear + '.' + zeroFormat(currentMonth));

        // 지난달 마지막 날짜, 요일 구하기
        var startDay = new Date(currentYear, currentMonth - 1, 0);
        var prevDate = startDay.getDate();
        var prevDay = startDay.getDay();

        // 이번 달의 마지막 날짜, 요일 구하기
        var endDay = new Date(currentYear, currentMonth, 0);
        var nextDate = endDay.getDate();
        var nextDay = endDay.getDay();

        calendar = document.querySelector('.dates')
        calendar.innerHTML = '';

		// 이번달의 1일 위치를 맞추기 위해 이전달의 날짜들 위치시키기
        for (var i = prevDate - prevDay + 1; i <= prevDate; i++) {
            calendar.innerHTML = calendar.innerHTML + '<div class="day prev disable"></div>'
        }

		// 이번달의 날짜 위치시키기
        for (var i = 1; i <= nextDate; i++) {
            calendar.innerHTML = calendar.innerHTML + '<button type="button" id=' + i + ' class="day current btn-light" data-bs-toggle="modal" data-bs-target="#dateModal" onclick="modal(this.id);">' + i + '</button> ';
        }
    }
    
    // 이전달로 이동
    function goPrev() {
        currentMonth -= 1
        if (currentMonth == 0) {
            currentYear -= 1;
            currentMonth = 12;
        }
        renderCalendar(currentYear, currentMonth, 1);
    }

    // 다음달로 이동
    function goNext() {
        currentMonth += 1
        if (currentMonth == 13) {
            currentYear += 1;
            currentMonth = 1;
        }
        renderCalendar(currentYear, currentMonth, 1);
    }
    
    function zeroFormat(value) {
        if (value / 10 < 1) {
            return '0' + value;
        } else {
            return value;
        }
    }
</script>

 

3-2. 코드 톺아보기 - 한국 표준시간 데이터 가져오기 

    var currentYear = ''; // 현재 페이지에 있는 연도
    var currentMonth = ''; // 현재 페이지에 있는 월
    var currentDate = ''; // 현재 페이지에 있는 일
    $(document).ready(function() {
        // 한국 표준시간 가져오기
        var date = new Date();
        var utc = date.getTime() + (date.getTimezoneOffset() * 60 * 1000);
        var kstGap = 9 * 60 * 60 * 1000;
        var today = new Date(utc + kstGap);
        var koreaDate = new Date(today.getFullYear(), today.getMonth(), today.getDate());

        // 연, 월, 일 가져오기
        currentYear = koreaDate.getFullYear();
        currentMonth = koreaDate.getMonth() + 1;
        currentDate = koreaDate.getDate();

        // 페이지 로딩 초기에 달력 가져오기
        renderCalendar(currentYear, currentMonth, currentDate);
    });
  • currentYear, currentMonth, currentDate는 현재, 이전달, 다음달에도 사용할 것이기 때문에 전역변수로 설정했다.
  • 한국 표준시간의 달력이 필요하므로 한국 시간으로 계산해 Date 객체를 생성한다. 
  • 주의할 점은 month가 0부터 시작하기 때문에 숫자를 맞추기 위해서 month 값에 +1을 꼭 해주어야 한다.

 

필요한 year, month, date를 renderCalendar()에 넘겨 달력을 만든다.

 

3-3. 코드 톺아보기 - 캘린더 로드하기

    function renderCalendar(currentYear, currentMonth, currentDate) {
    	// zeroFormat()은 따로 정의한 함수, 2024-5를 2024-05로 만들어주는 역할
        $('#year-month').text(currentYear + '.' + zeroFormat(currentMonth));

        // 지난달 마지막 날짜, 요일 구하기
        var startDay = new Date(currentYear, currentMonth - 1, 0);
        var prevDate = startDay.getDate();
        var prevDay = startDay.getDay();

        // 이번 달의 마지막 날짜, 요일 구하기
        var endDay = new Date(currentYear, currentMonth, 0);
        var nextDate = endDay.getDate();
        var nextDay = endDay.getDay();

        calendar = document.querySelector('.dates')
        calendar.innerHTML = '';

		// 이번달의 1일 위치를 맞추기 위해 이전달의 날짜들 위치시키기
        for (var i = prevDate - prevDay + 1; i <= prevDate; i++) {
            calendar.innerHTML = calendar.innerHTML + '<div class="day prev disable"></div>'
        }

		// 이번달의 날짜 위치시키기
        for (var i = 1; i <= nextDate; i++) {
            calendar.innerHTML = calendar.innerHTML + '<button type="button" id=' + i + ' class="day current btn-light" data-bs-toggle="modal" data-bs-target="#dateModal" onclick="modal(this.id);">' + i + '</button> ';
        }
    }

이번달의 전체 날짜를 알게 되었으니 <div class="dates">를 가져와서 날짜를 입력하면 된다. 이때 고려해야할 점은 1일의 요일 위치인데, 이전달을 고려하지 않고 이번달 데이터만 html에 추가하면 1일의 요일이 맞지 않게 된다. 

 

따라서 이전달 정보를 가지고 와서 이번달 1일 전에 태그를 추가함으로써 이번달 1일의 요일을 맞출 수 있다.

// 지난달 마지막 날짜, 요일 구하기
var startDay = new Date(currentYear, currentMonth - 1, 0);
var prevDate = startDay.getDate();
var prevDay = startDay.getDay();
        
// 이번달의 1일 위치를 맞추기 위해 이전달의 날짜들 위치시키고 보이지 않게 disable하기
for (var i = prevDate - prevDay + 1; i <= prevDate; i++) {
    calendar.innerHTML = calendar.innerHTML + '<div class="day prev disable"></div>'
}

 

3-4. 이전달, 다음달로 이동하기

    // 이전달로 이동
    function goPrev() {
        currentMonth -= 1
        if (currentMonth == 0) {
            currentYear -= 1;
            currentMonth = 12;
        }
        renderCalendar(currentYear, currentMonth, 1);
    }

    // 다음달로 이동
    function goNext() {
        currentMonth += 1
        if (currentMonth == 13) {
            currentYear += 1;
            currentMonth = 1;
        }
        renderCalendar(currentYear, currentMonth, 1);
    }

이전달과 다음달로 캘린더 이동은 기본적으로 위에서 구현한 renderCalendar()에 필요한 날짜를 넘겨주면 쉽게 구현할 수 있다. 여기서 주의해야할 점은 날짜 계산을 어떻게 할 지인데, 1년은 1월부터 12월까지이고, 1월 이전에는 전년도의 12월이, 12월의 다음달은 다음년도의 1월이기 때문이다. 이는 먼저 월을 계산한 다음에 이전 년도나 다음년도로 넘어간다면 따로 currentYear, currentMonth를 재설정하는 방식으로 해결했다.

 

 

구현 화면

위 코드를 적용하면 날짜 정보는 동적으로 가져오지만, 아래처럼 우리가 아는 달력 형태가 아닌 데이터가 나열된 형태로만 확인할 수 있다.

 

4.  css로 달력 모양 만들기

/resources/static/css/index.css

.calendar {
    padding-top: 60px;
    position: relative;
    margin: 0 auto;
}

.calendar .days {
    display: flex;
    margin-bottom: 20px;
    padding-bottom: 20px;
    border-bottom: 1px solid #ddd;
}

.calendar .day {
    display:flex;
    align-items: center;
    justify-content: center;
    width: calc(100% / 7);
    text-align: left;
    color: #999;
    font-size: 12px;
    text-align: center;
    border-radius:5px
}

.calendar .dates {
    display: flex;
    flex-flow: wrap;
    height: 290px;
}

 

구현 화면

 

위 css 코드를 적용한 다음에 화면이다. 우리가 알고 있는 일반 달력화면이 되었다! 참고한 블로그에서는 월요일부터 시작하는 달력으로 만들었지만 일요일부터 시작하는 달력을 만들고 싶으면 커스터마이징하면 될 것 같다.

 

 

참고

자바스크립트로 달력 만들기

 

1. 고민

가계부 프로젝트에서 핵심은 지출내역 저장/수정/삭제라고 생각하는데, 수정 화면을 만들 때 고민이 많이 되었다. 가장 고민이 되었던 부분은 수정 페이지로 넘어갔을 때 해당 리소스의 데이터를 어떻게 가져올 것인지였다. 등록 화면은 기존 데이터가 있을 필요없이 그냥 새로운 페이지를 만들면 되는데, 수정 화면은 기존의 리소스 데이터를 가져와서 보여준 뒤 사용자가 변경한 것을 저장하는 방식으로 진행을 해야했다. 

 

진행하면서 가장 고민이 되었던 부분은 기존의 리소스 데이터를 어떤 로직으로 가져와서 보여줄 것인지였다. 처음에는 화면 로딩 시 ajax로 데이터를 가져와서 반영하려고 했으나 아직 미숙해서 그런지 잘되지 않았다. 그래서 이전에 배웠던 Thymeleaf의 th:value 문법Controller에서 Model 객체로 수정하려는 리소스의 데이터를 담아서 화면에 넘겨주는 방식을 선택했다.

 

2. 순서

진행 순서는 다음과 같다.

 

1. '수정' 버튼 클릭

2. 수정화면으로 이동 + 클라이언트는 url로 리소스(지출내역) id를 서버로 전달

3. 서버는 받은 id로 리소스 정보를 db에서 가져와 Controller 단에서 Model에 담아서 전달

4. model에 있는 정보를 화면에서 보여주기

 

2-1. '수정' 버튼 클릭

// 수정 버튼 클릭시 modifyDailyPayment(this.id) 호출
// 여기서 this.id는 리소스의 id가 되게 설정했다
`<button class="btn btn-success btn-payment" id="`+ paymentId + `" onclick="modifyDailyPayments(this.id);">수정</button></li> `;

 

2-2. 수정화면으로 이동 + 클라이언트는 url로 리소스(지출내역) id를 서버로 전달

// 해당 리소스의 수정 화면으로 이동
function modifyDailyPayments(paymentId) {
  location.replace("http://localhost:8080/expenditure/" + paymentId);
}

 

2-3. 서버는 받은 id로 리소스 정보를 db에서 가져와 Controller 단에서 Model에 담아서 전달

@Controller
@RequiredArgsConstructor
public class AccountBookController {    
    
    @GetMapping("/expenditure/{paymentId}")
    public String modifyExpenditure(@PathVariable long paymentId, Model model) {
        GetDailyPaymentsResponseDto payment = dailyPaymentsService
                .getDailyPayment(paymentId);
                
        model.addAttribute("payment", payment);
        return "edit-expenditure";
    }
}

dailyPaymentsService에서 id로 리소스를 식별해 정보를 GetDailyPaymentsReponseDto 객체에 저장한 뒤, Model을 통해 "payment"라는 이름으로 해당 정보를 view로 넘겨준다.

 

- GetDailyPaymentsResponseDto.class

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GetDailyPaymentsResponseDto {

    private long dailyPaymentId;
    private Integer paidAmount;
    private String payLocation;
    private String methodOfPayment;
    private Long categoryId;
    private String categoryName;
    private String memo;
    private String date;
    
    public static GetDailyPaymentsResponseDto of(DailyPayments dailyPayments, String categoryName) {
        return GetDailyPaymentsResponseDto
                .builder()
                .dailyPaymentId(dailyPayments.getId())
                .paidAmount(dailyPayments.getPaidAmount())
                .payLocation(dailyPayments.getPayLocation())
                .methodOfPayment(dailyPayments.getMethodOfPayment())
                .categoryId(dailyPayments.getCategoryId())
                .categoryName(categoryName)
                .memo(dailyPayments.getMemo())
                .date(dailyPayments.getDate())
                .build();
    }
}

 

- model.addAttribute()

model.addAttribute("payment", payment);

 

 

필요한 정보를 view에 모두 보냈으면, 수정화면으로 이동하기 위해 "edit-expenditure" 문자열을 반환하면 된다.

 

 

2-4. model에 있는 정보를 화면에서 보여주기

위의 과정을 거치면 기존에 저장되어있던 지출 정보를 수정 화면에서 확인할 수 있다.

아직 드롭박스에 반영하는 법은 잘 모르겠다. 추후 적용 예정!

 

코드

위에서 서버에서 가져온 지출내역을 payment라는 이름으로 view에 넘겼다. 그렇기에 thymeleaf 문법을 이용해 payment의 정보를 적재적소에 위치시켜보자.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="layout/default_layout">
<th:block layout:fragment="content">

    <div id="top" class="container text-center">
        <div class="row">
            <h3>지출 수정하기</h3>
        </div>
    </div>

    <div id="middle1" class="container text-center" style="padding-top:50px;">
        <div class="row">
            <div class="col-6">금액<input id="paid-amount" th:value="${payment.paidAmount}"></div>
            <div class="col-6">
                카테고리
                <button class="btn btn-light" type="button" th:text="${payment.categoryName}" id="category-radio" data-bs-toggle="modal" data-bs-target="#categoryModal" onclick="getCategoryList();" ></button>
            </div>
        </div>
    </div>

    <div id="middle2" class="container text-center" style="padding-top:50px;">
        <div class="row">
            <div class="col-6">거래처<input id="pay-location" th:value="${payment.payLocation}"></div>
            <div class="col-6">결제 수단<input id="method-payment" th:value="${payment.methodOfPayment}"></div>
        </div>
    </div>

    <div id="middle3" class="container text-center" style="padding-top:50px;">
        <div class="row">
            <div class="col-6">메모<input id="memo" th:value="${payment.memo}"></div>
            <div class="col-6">날짜
                <select id="date-select" class="form-select" onclick="getDate();" th:text="${payment.date}"></select>
            </div>
        </div>
    </div>
    
    ...
    
<th:block>
</html>

 

 

기본적인 thymeleaf 문법은 아래 th:value=..  형식을 따르면 된다.

<input id="paid-amount" th:value="${payment.paidAmount}">

 

 

2-5. 수정 완료

이제 원하는 만큼 정보를 변경한 뒤 아래 "수정하기" 버튼을 클릭하면 수정 api를 전송하면서 수정이 완료된다.

1. 계기

ajax를 사용하게된 계기는 클라이언트와 서버를 모두 개발하면서 클라이언트-서버 간 http 메시지 통신을 하고싶었기 때문이다. Thymeleaf를 사용했던 터라 form 태그로 할 수도 있었지만, 클라이언트 동작방식을 더 이해하고 싶어서 ajax를 선택했다.

 

1-1. ajax란?

ajax는 JavaScript를 사용한 비동기 통신, 클라이언트-서버 간 xml, json 데이터를 주고 받는 기술이다. ajax는 비동기로 동작하기 때문에 전체 페이지를 새로 고치지 않고 뷰를 갱신할 수 있다.

 

1-2. ajax 사용 이유

페이지 전체를 새로고침하지 않고 web 화면에서 데이터를 조회하고 싶은 경우에 사용할 수 있다. json 이나 xml 형태로 필요한 데이터만 받아 갱신하기 때문에 그만큼의 자원과 시간을 아낄 수 있다.

 

1-3. ajax 진행과정

1) XMLHttpRequest Object를 만든다

브라우저에게 request를 보낼 준비 시키는 과정이다.

이를 위해 필요한 method를 갖춘 object가 필요하다.

 

2) callback 함수를 만든다

callback 함수는 어떤 이벤트에 의해 호출되는 함수를 의미한다.

서버에서 response가 왔을 때 callback 함수를 실행한다.

HTML 페이지를 업데이트 한다.

 

1-4. ajax 사용법

ajax를 사용하기 위해서는 jquery를 import 해야한다. 

 

build.gradle

implementation 'org.webjars:jquery:3.1.1-1'

 

헤더

<head>
	<script src="/webjars/jquery/jquery.min.js"></script>
</head>

 

ajax 코드

<script>
function completeAuthEmail() {
    const parmas = {}; // body에 넣을 json 데이터

    $.ajax({
        url : url, // 메시지 보낼 url
        type : 'post',
        contentType : 'application/json; charset=utf-8;',
        dataType : 'json',
        data : JSON.stringify(params), // json 데이터를 JSON.stringify()를 해야 에러가 발생하지 않는다.
        success : function(response) {
            alert('인증이 완료 되었습니다.');
        },
        error : function(response, status, error) {
            alert('에러 발생');
        }
    })
}
</script>

기본적으로 HTTP 메시지를 주고 받는 것이기 때문에 HTTP 메시지 구조(start line, header, body)로 코드를 작성해야 한다. 위 코드에서 보이는 필드 중 url, type은 start line에, contentType, dataType은 header에, data는 body에 해당한다. 그렇기에 각각에 맞는 내용들을 입력하면 된다. 메시지를 보낸 후 성공 응답인 경우는 succes, 실패 응답이라면 error에 선언한 콜백함수가 실행된다.

 

여기서 주의해야할 점은 body에 해당하는 data에 해쉬 그대로 넣으면 에러가 발생한다. 그래서 JSON.stringify()로 형변환을 한 뒤 전송해야 한다.

 

  • XMLHttpRequest 객체를 얻은 뒤, url을 통해 요청하고 응답을 받으면 응답 결과에 맞는 함수를 실행하는 구조이다
  • 효율적인 Ajax 사용을 위해 Jquery에서 구현한 ajax 기능을 사용했다

 

구체적인 예시 - 인증 이메일 전송하기

function completeAuthEmail() {
    var email = $('#email').val();
    var authKey = $('#authKey').val();
    const params = {
        'email' : email,
        'authKey' : authKey
    }

    $.ajax({
        url : `/v1/auth/email/complete`,
        type : 'post',
        contentType : 'application/json; charset=utf-8;',
        dataType : 'json',
        data : JSON.stringify(params),
        success : function(response) {
            alert('인증이 완료 되었습니다.');
        },
        error : function(response, status, error) {
            alert(JSON.parse(response.responseText).message);
        }
    })
}

기본 ajax 구조를 가지고 실제로 작성했던 코드이다. 구조는 동일하지만, body에 들어가는 구체적인 값을 jQuery로 가져오고, 실제 사용하는 api url을 추가했다. 각 상황에 맞게 성공일 때 동작방식과 실패일 때 동작방식을 설정해주면 된다. 

 

여기서 주의해야할 점은 응답 메시지에서 JSON key를 가져오는 방법이다. 응답 body의 메시지를 가져오고 싶은데 처음에는 response['message'] 와 같은 형식으로 데이터를 가져오려고 했는데 에러가 발생하고 원하는 값을 가져오지 못했다. 찾아보니 response가 Object이므로 '.'으로 바디를 꺼낸 뒤 JSON으로 파싱을 해야 응답 바디에서 원하는 key의 값을 가져올 수 있다.

// response body
{ status : NOT_FOUND,
  message : "존재하지 않는 이메일입니다."
}

// response body에 있는 message 키의 값 가져오는 코드
JSON.parse(response.responseText).message

 

 

참고

[JQUERY] SpringBoot - ajax 사용법 및 예제(+thymeleaf)

 

+ Recent posts