mini Projects/ToyTalk 키덜트 대화방 서비스

(25.06.02) Thymleaf를 통한 회원가입/로그인 FE 구현

Genie; 2025. 6. 3. 12:51

 

 

(25.05.30) ToyTalk WebSocket을 활용한 Chat 기능 구현 (Spring Boot

• 키덜트 정보를 플랫폼 구애없이 대화방에서 나눌수 있는 특화된 채팅 플랫폼의 니즈• WebSocket 프레임 구현 서비스 구축기획목적">키덜트 사용자들이 공통 주제로 자유롭게 대" data-og-host="andr

andrew75313.tistory.com

이전에 진행했던 WebSocket을 통한 대화방을 작동시키기 위해서는
사용자들이 인증받은 사용자 -> 즉, 회원가입과 로그인을 통해 JWT 발급이 되어 인증될 수 있는 사용자 여야했다.

따라서,

채팅방 FE 단을 구현하기 위해서는 회원가입, 로그인 FE 구현을 통해서 사용자가 등록되어야해, 작성을 했다.

 


ToyTalk 의 SSR(Server Side Rendering) 을 위한 Thymleaf 사용

  • Thymeleaf는 HTML 템플릿 엔진으로, ToyTalk의 Spring MVC구조와 빠르게 통합되며 서버사이드 렌더링에 적합
  • 별도의 React 등 프론트엔드 프레임워크 없이도 전체 페이지를 서버에서 렌더링하여 사용자에게 전달 용이
    • 추후 Docker를 통해 간단하게 배포하고자 했으며, 프론트엔드와 백엔드가 통합된 SSR 구조가 배포 및 유지보수 측면에서 효율적
  • 로그인, 회원가입 등 인증 처리가 서버에서 이루어지므로 쿠키 기반 JWT 인증과도 자연스럽게 연동 가능
    • JWT 인증을 쿠키 기반으로 구성할 때, SSR 구조가 인증 흐름 관리에 더 적합하다고 판단
  • Docker 배포를 고려한 구조 설계
    • Spring Boot + Thymeleaf 구조는 FE/BE가 통합된 형태로, Docker 이미지로 쉽게 컨테이너화로 간단한 프로젝트 구현 가능

FORM을 통한 회원가입 FE 구현

  • Controller에서 기존의 ResponseEntity를 통해 JSON 타입으로 응답했던 것을 Model 객체로 데이터 → HTML 템플릿으로 전달해서 랜더링 할 수 있도록 변경
@Controller
@RequiredArgsConstructor
@RequestMapping("/api/users")
public class UserController {

  ...

    @GetMapping("/signup")
    public String signupPage() {
        return "signup";
    }

    @PostMapping("/signup")
    public String signup(@Valid SignupRequestDTO requestDTO,
                         BindingResult bindingResult,
                         RedirectAttributes redirectAttributes,
                         Model model) {
        if (bindingResult.hasErrors()) {
            model.addAttribute("errorMessage", bindingResult.getAllErrors().get(0).getDefaultMessage());
            return "signup";
        }

        try {
            userService.createUser(requestDTO);
        } catch (Exception e) {
            model.addAttribute("errorMessage", e.getMessage());
            return "signup";
        }

        redirectAttributes.addFlashAttribute("successMessage", "회원가입이 완료되었습니다. 로그인해주세요.");
        return "redirect:/api/users/login";
    }
  • model을 통한 FORM구조를 사용하기 때문에 RestController, @RequestBody 모두 사용 할 수 없음
  • model.addAttribute를 통해, 에러가 발생하더라도 Console로 찍히게 하는 것 외에, 알럿으로 사용자에게 보여질 수 있도록 모델로 전달
  • 완료가 되었을 경우 login.html 페이지인 /api/users/login 로 redirect할 수 있도록 구현
    @GetMapping("/signup")
    public String signupPage() {
        return "signup";
    }
  • 특정 엔드포인트를 요청하면 signup.html이 보여질 수 있도록 Controller를 추가해야함
    • SecurityConfig에서 해당 접근은 회원가입이기 때문에 Authenticated 설정을 추가로 진행

signup.html

회원가입 화면

<body>
<div id="login-form">
  <div id="login-title">ToyTalk 회원가입</div>

  <form action="/api/users/signup" method="post">
    <div class="login-id-label">아이디</div>
    <input type="text" name="username" placeholder="USERNAME" class="login-input-box">

    <div class="login-id-label">비밀번호</div>
    <input type="password" name="password" placeholder="PASSWORD" class="login-input-box">

    <div class="login-id-label">이메일</div>
    <input type="text" name="email" placeholder="E-MAIL" class="login-input-box">

    <button id="login-id-submit">회원 가입</button>
  </form>
</div>

<!-- 에러 메시지 알림 -->
<script th:if="${errorMessage}">
  alert('[[${errorMessage}]]');
</script>
  • SignupRequestDTO로 요청하기 윟새 해당 form을 작성해서 회원가입 요청
    • 위에서 언급했듯이 에러메시지 알럿 처리

FORM+AJAX 를 통한 일반 회원가입 FE 구현

로그인 화면

  • 폼 로그인으로 요청을 했지만, 받아오는 내용은 Spring Security가 받아오면서 Header에 Access Token과 Refresh Token을 넣어서 JSON으로 응답하므로 AJAX로 응답을 받게 됨
    • FORM은 데이터를 전달하기 때문에, JWT 가 쿠키로 전달되지 않는 이상 헤더로 요청할 수 없음

참고) Spring Security의 AuthenticationSuccessHandler

  • HttpServletResponse을 통해서 JWT를 전달하고 있음
...
				response.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);
        response.addHeader(JwtUtil.REFRESHTOKEN_HEADER, refreshToken);

        response.setContentType("application/json;charset=UTF-8");
        String json = new ObjectMapper().writeValueAsString(
                Map.of("statusCode", 200, "msg", "로그인 성공")
        );

        response.getWriter().write(json);

JWT 추출, 저장 Script

$(document).ready(function () {
    // 로그인 시 응답 헤더에서 토큰 저장
    $(document).ajaxSuccess(function (event, xhr, settings) {
        const accessToken = xhr.getResponseHeader('Authorization');
        const refreshToken = xhr.getResponseHeader('refreshToken');

        if (accessToken) {
            localStorage.setItem('accessToken', accessToken);
        }
        if (refreshToken) {
            localStorage.setItem('refreshToken', refreshToken);
        }
    });

    // 모든 요청에 accessToken 자동 첨부
    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            const excludedUrls = [
                '/api/login',
                '/api/users/signup',
                '/',                // 랜딩 페이지
                '/api/logout'       // 필요 시 로그아웃도 제외 가능
            ];
            const isExcluded = excludedUrls.some(url => settings.url.startsWith(url));

            if (!isExcluded) {
                const token = localStorage.getItem('accessToken');
                if (token) {
                    xhr.setRequestHeader('Authorization', token);
                }
            }
        }
    });
});
  • Redirect 가 되어버리면 Header로 전달받은 JWT들이 전부 없어지게 되어버림
    • 따라서, 로컬 저장소에 저장해 이를 사용할 수 있는 전역 스크립트를 작성

로그인 FLOW

  1. 클라이언트에서 /api/login으로 로그인 요청
  2. 서버에서 로그인 성공 시, 응답 헤더에 Authorization (Access Token)과 refreshToken을 담아서 클라이언트로 보냄
  3. jQuery의 ajaxSuccess 핸들러가 이 응답을 가로채서 실행
  4. 핸들러 내부의 코드 (xhr.getResponseHeader('Authorization'), xhr.getResponseHeader('refreshToken'))를 통해 응답 헤더에서 Access Token, Refresh Token 토큰 값을 읽어오기
  5. 읽어온 토큰 값을 클라이언트의 localStorage.setItem()을 사용하여 저장
  6. 저장된 토큰은 앞으로 ToyTalk 서버에 요청 시 사용

최종 일반 회원가입~로그인 화면 구현

 

전체 FLOW 영상

 


Thymleaf를 사용할 수는 있었지만,

OAuth 2.0를 포함해 HttpOnly 여도 쿠키를 통한 JWT 전달 방법이 보안상 제한적일 수 있다는 추세로,

쿠키대신 Header로 발급받은 JWT를 Thymleaf 렌더링 HTML에서 가지고 오는 것을 찾고, 구현하는데 꽤 애를 먹었다.

 

특히, 카카오 로그인일 경우 백엔드로 구성되어있으나, FE에서 Kakao 인증화면에서 다시 ToyTalk Redirect 할 때마다, 응답받은 내용을  동일 출처 정책 (Same-Origin Policy, SOP)에 의해 전달할 수 없기 때문에.. 계속 오류가 발생해 이는 추후 메인 화면(대화방 리스트 확인 화면) 과 추가할 계획 이다.