본문 바로가기

spring

Jlaner 개발기록 6 오류 처리

이번 포스팅은 오류 처리한 부분에 대해서 다루고 넘어가도록 하겠다.

전체적인 로직 점검과 오류 처리에 대한 부분을 명확하게 처리하도록 구성했다.

 

오류 처리를 한 부분과 어떻게 처리를 했는지 확인해 보자.

가장 먼저 보안 설정인 securityconfig를 확인해 보았다.

package com.jlaner.project.config;


import com.jlaner.project.config.jwt.TokenAuthenticationFilter;
import com.jlaner.project.config.jwt.TokenProvider;
import com.jlaner.project.config.outh2.*;
import com.jlaner.project.repository.MemberRepository;
import com.jlaner.project.service.MemberService;
import com.jlaner.project.service.RefreshTokenRedisService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {

    private final TokenProvider tokenProvider;
    private final MemberRepository memberRepository;
    private final MemberService memberService;
    private final CustomOauth2UserService customOauth2UserService;
    private final RefreshTokenRedisService refreshTokenRedisService;
    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

    @Bean
    public WebSecurityCustomizer configure() { // 스프링 시큐리티 기능 비활성화
        return (web) -> web.ignoring()
//                .requestMatchers(toH2Console())
                .requestMatchers(
                        new AntPathRequestMatcher("/img/**"),
                        new AntPathRequestMatcher("/css/**"),
                        new AntPathRequestMatcher("/js/**")
                );
    }

    // HTTP 보안 설정을 구성하는 메서드
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
         http
                .csrf(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests(auth -> auth
                        .requestMatchers(
                                new AntPathRequestMatcher("/login/**"),
                                new AntPathRequestMatcher("/pngs/**"),
                                new AntPathRequestMatcher("/error/**"),
                                new AntPathRequestMatcher("/favicon.ico"),
                                new AntPathRequestMatcher("/h2-console/**"),
                                new AntPathRequestMatcher("/actuator/**"))
                        .permitAll() // 위 경로들은 인증 없이 접근 가능
                        .requestMatchers(
                                new AntPathRequestMatcher("/api/**")
                        )
                        .authenticated()
                        .anyRequest()
                        .permitAll()
                )
                .oauth2Login(login -> login
                        .loginPage("/login")
                        .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.authorizationRequestRepository(oAuth2AuthorizationRequestBasedOnCookieRepository()))
                        .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userService(customOauth2UserService))
                        .successHandler(oAuth2SuccessHandler())
                )
                 .exceptionHandling(exceptionHandling -> exceptionHandling
                         .authenticationEntryPoint(customAuthenticationEntryPoint));


         return http.build();
    }

    @Bean
    public OAuth2SuccessHandler oAuth2SuccessHandler() {
        return new OAuth2SuccessHandler(tokenProvider,
                refreshTokenRedisService,
                oAuth2AuthorizationRequestBasedOnCookieRepository(),
                memberService
        );
    }


    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter(tokenProvider,refreshTokenRedisService,memberRepository);
    }



    @Bean
    public OAuth2AuthorizationRequestBasedOnCookieRepository oAuth2AuthorizationRequestBasedOnCookieRepository() {
        return new OAuth2AuthorizationRequestBasedOnCookieRepository();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

처리 중 Exception 발생 시 ExceptionHandling으로 아래 클래스에서 처리하게 된다.

package com.jlaner.project.config.outh2;


import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // 로그인 페이지로 리다이렉트
        response.sendRedirect("/login?error=unauthorized");
    }
}

 

다음으로 필터에서 한 오류 처리를 확인하자.

package com.jlaner.project.config.jwt;


import com.jlaner.project.domain.Member;
import com.jlaner.project.domain.RefreshToken;
import com.jlaner.project.exception.CustomUnauthorizedException;
import com.jlaner.project.repository.MemberRepository;
import com.jlaner.project.service.RefreshTokenRedisService;
import com.jlaner.project.util.CookieUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.time.Duration;


@RequiredArgsConstructor
@Slf4j
@Order(2)
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final TokenProvider tokenProvider;
    private final RefreshTokenRedisService refreshTokenRedisService;
    private final MemberRepository memberRepository;
    public static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token";
    public static final Duration ACCESS_TOKEN_DURATION = Duration.ofDays(1);
    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer ";
    private int count =0;
    /**
     * 요청을 필터링하여 JWT 토큰을 검증하고 인증 정보를 설정하는 메서드
     *
     * @param request     클라이언트에서 온 HTTP 요청
     * @param response    서버로부터 클라이언트로 응답
     * @param filterChain 다음 필터로 진행할 수 있도록 체인을 제공하는 객체
     * @throws ServletException 필터 처리 중 발생할 수 있는 예외
     * @throws IOException      입출력 예외
     */
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {

        String requestURI = request.getRequestURI();

        //정적 리소스에 대해서는 필터를 무시
        if (requestURI.startsWith("/css") || requestURI.startsWith("/pngs") || requestURI.startsWith("/js")) {
            filterChain.doFilter(request, response);
            return;
        }
        log.info("Incoming request: URI = {}, Method = {}", request.getRequestURI(), request.getMethod());
        log.info("request={}", request);
        log.info("requestURI={}", request.getRequestURI());


        // HTTP 요청 헤더에서 Authorization 헤더 값을 가져옴
        String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
        log.info("authorizationHeader ={}", authorizationHeader);
        // Authorization 헤더 값에서 토큰을 추출
        String token = getAccessToken(authorizationHeader);
        log.info("token = {}", token);

        // 추출된 토큰이 유효한 경우
        if (tokenProvider.validToken(token)) {
            log.info("인증 성공");
            // 토큰을 사용하여 인증 객체를 가져옴
            Authentication authentication = tokenProvider.getAuthentication(token);
            // SecurityContext에 인증 객체를 설정하여 인증 상태를 유지
            SecurityContextHolder.getContext().setAuthentication(authentication);
            filterChain.doFilter(request, response);
            return;
        }


        if (token != null && !tokenProvider.validToken(token)) {
            log.info("Access Token 만료");
            handleExpiredAccessToken(request, response, filterChain);
            return;
        }

        // 다음 필터로 요청과 응답을 전달
        filterChain.doFilter(request, response);
}

    private void handleExpiredAccessToken(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        String refreshTokenValue = CookieUtil.getCookie(request, REFRESH_TOKEN_COOKIE_NAME)
                .map(Cookie::getValue)
                .orElse(null);
        log.info("Refresh Token = {}", refreshTokenValue);

        if (refreshTokenValue != null) {
            RefreshToken refreshToken = refreshTokenRedisService.findByRefreshToken(refreshTokenValue);

            if (refreshToken != null && tokenProvider.validToken(refreshTokenValue)) {
                Member member = memberRepository.findById(refreshToken.getMemberId()).orElse(null);

                if (member != null) {
                    String newAccessToken = tokenProvider.generateToken(member, ACCESS_TOKEN_DURATION);
                    refreshToken.accessTokenUpdate(newAccessToken);
                    refreshTokenRedisService.saveToken(refreshToken);

                    log.info("새로운 액세스 토큰 발급 = {}", newAccessToken);

                    response.setHeader(HEADER_AUTHORIZATION, TOKEN_PREFIX + newAccessToken);

                    Authentication authentication = tokenProvider.getAuthentication(newAccessToken);
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    // 발급 후 필터 체인 진행
                    filterChain.doFilter(request, response);
                } else {
                    log.info("Not Found Member");
                    response.sendRedirect("/login?error=memberNotFound");
                }
            } else {
                log.info("Invalid Refresh Token");
                response.sendRedirect("/login?error=invalidRefreshToken");
            }
        } else {
            log.info("Missing Refresh Token");
            response.sendRedirect("/login?error=missingRefreshToken");
        }
    }



    /**
     * Authorization 헤더에서 JWT 토큰을 추출하는 메서드
     *
     * @param authorizationHeader Authorization 헤더 값
     * @return 추출된 JWT 토큰
     */
    private String getAccessToken(String authorizationHeader) {
        if (authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)) {
            return authorizationHeader.substring(TOKEN_PREFIX.length());
        }
        return null;
    }
}

 

각 검증에서 검증이 되지 않을 때 login으로 리다이렉트 하게 구성했으며 리다이렉트 할 때 error로 파라미터를 넘겨줘서 처리하도록 만들었다.

 

package com.jlaner.project.controller;


import com.jlaner.project.domain.Member;

import com.jlaner.project.domain.RefreshToken;

import com.jlaner.project.dto.PostDto;
import com.jlaner.project.dto.ScheduleAndPostDto;
import com.jlaner.project.dto.ScheduleDataDto;
import com.jlaner.project.service.MemberService;
import com.jlaner.project.service.PostService;
import com.jlaner.project.service.RefreshTokenRedisService;
import com.jlaner.project.service.ScheduleService;
import com.jlaner.project.util.CookieUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.*;

import java.util.Date;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class JlanerController {

    private final RefreshTokenRedisService refreshTokenRedisService;
    private final MemberService memberService;
    private final PostService postService;
    private final ScheduleService scheduleService;


    @PostMapping("/jlaner/post/data")
    public ResponseEntity<?> savePostData(@RequestBody PostDto postDto,
                                          HttpServletRequest request,
                                          HttpServletResponse response
    ) {
        try {
            
        String refreshToken = CookieUtil.getCookie(request, "refresh_token")
                .map(Cookie::getValue)
                .orElse(null);

        RefreshToken findByRefreshToken = refreshTokenRedisService.findByRefreshToken(refreshToken);
        Long findMemberId = findByRefreshToken.getMemberId();

        Member findMember = memberService.findByMemberId(findMemberId);

        postService.postDataSaveOrUpdate(postDto,findMember);

        log.info("데이터가 저장되었습니다.{}", postDto.getContentData());
        log.info("date={}", postDto.getScheduleDate());

        return ResponseEntity.status(200).build();
        }catch (Exception e) {
            return ResponseEntity.status(401).build();
        }
    }

    @PostMapping("/jlaner/schedule/data")
    public ResponseEntity<?> saveScheduleData(@RequestBody ScheduleDataDto scheduleDataDto,
                                          HttpServletRequest request,
                                          HttpServletResponse response
    ) {
        try {
        String refreshToken = CookieUtil.getCookie(request, "refresh_token")
                .map(Cookie::getValue)
                .orElse(null);

        RefreshToken findByRefreshToken = refreshTokenRedisService.findByRefreshToken(refreshToken);
        Long findMemberId = findByRefreshToken.getMemberId();

        Member findMember = memberService.findByMemberId(findMemberId);

        scheduleService.scheduleDataSaveOrUpdate(scheduleDataDto, findMember);

        return ResponseEntity.status(200).build();

        } catch (Exception e) {
            return ResponseEntity.status(401).build();
        }
    }

    @GetMapping("/jlaner/data")
    public ResponseEntity<?> getMemberDateData(@RequestParam("date") @DateTimeFormat(pattern = "yyyy-MM-dd") Date date,
                                               HttpServletRequest request) {

        try {

            String refreshToken = CookieUtil.getCookie(request, "refresh_token")
                    .map(Cookie::getValue)
                    .orElse(null);

            RefreshToken findByRefreshToken = refreshTokenRedisService.findByRefreshToken(refreshToken);
            Long findMemberId = findByRefreshToken.getMemberId();

            ScheduleDataDto findScheduleData = scheduleService.findByScheduleDate(date, findMemberId);
            PostDto findPostData = postService.findByPostDate(date, findMemberId);

            ScheduleAndPostDto sendData = new ScheduleAndPostDto(findScheduleData, findPostData);

            log.info("sendData={}", sendData.getPostData().getScheduleDate());
            log.info("sendData={}", sendData.getPostData().getContentData());
            log.info("sendData={}", sendData.getScheduleData().getScheduleDate());
            log.info("sendData={}", sendData.getScheduleData().getScheduleContent1());
            log.info("sendData={}", sendData.getScheduleData().isCheckBox1());

            return ResponseEntity.ok(sendData);
        } catch (Exception e) {
            return ResponseEntity.status(401).build();
        }


    }


}

 

controller에서는 401을 반환하게 했다.

package com.jlaner.project.controller;

import com.jlaner.project.config.jwt.TokenProvider;
import com.jlaner.project.domain.Member;
import com.jlaner.project.domain.RefreshToken;
import com.jlaner.project.service.MemberService;
import com.jlaner.project.service.RefreshTokenRedisService;
import com.jlaner.project.util.CookieUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.util.UriComponentsBuilder;

import java.time.Duration;

@Controller
@Slf4j
@RequiredArgsConstructor
public class PageController {
    private final TokenProvider tokenProvider;
    private final MemberService memberService;
    private final RefreshTokenRedisService refreshTokenRedisService;

    @GetMapping("")
    public String moveLogin(){
        return "home";
    }

    @GetMapping("/login")
    public String login() {

        return "login/login";
    }
    @GetMapping("/home")
    public String home(@RequestParam("token") String token,
                       HttpServletRequest request,
                       HttpServletResponse response,
                       Model model) {
        // accessToken을 로그에 출력하여 확인
        log.info("accessToken={}", token);

        try {
            Long memberId;
            String newAccessToken = null;

            if (tokenProvider.isTokenExpired(token)) {
                log.info("토큰이 만료되었습니다. 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급합니다.");
                String refreshTokenValue = CookieUtil.getCookie(request, "refresh_token")
                        .map(Cookie::getValue)
                        .orElse(null);

                if (refreshTokenValue != null) {
                    RefreshToken refreshToken = refreshTokenRedisService.findByRefreshToken(refreshTokenValue);
                    if (refreshToken != null) {
                        memberId = refreshToken.getMemberId();
                        Member findByMember = memberService.findByMemberId(memberId);

                        newAccessToken = tokenProvider.generateToken(findByMember, Duration.ofDays(1));
                        //새로운 액세스 토큰을 발급해야 하기 때문에 redis에 저장된 토큰 정보의 AccessToken를 업데이트
                        RefreshToken updateToken = refreshToken.accessTokenUpdate(newAccessToken);
                        refreshTokenRedisService.saveToken(updateToken);

                        response.setHeader("Authorization", "Bearer " + newAccessToken);
                        log.info("새로운 액세스 토큰 발급={}", newAccessToken);

                        // 새로운 토큰을 포함하여 리다이렉트
                        String targetUrl = UriComponentsBuilder.fromUriString("/home")
                                .queryParam("token", newAccessToken)
                                .build()
                                .toUriString();
                        return "redirect:" + targetUrl;
                    } else {
                        log.error("유효하지 않은 리프레시 토큰입니다.");
                        return "redirect:/login?error=invalidRefreshToken";
                    }
                } else {
                    log.error("리프레시 토큰이 존재하지 않습니다.");
                    return "redirect:/login?error=missingRefreshToken";
                }
            } else {
                memberId = tokenProvider.getMemberId(token);
            }

            Member findMember = memberService.findByMemberId(memberId);

            model.addAttribute("memberName", findMember.getName());
            model.addAttribute("contentData", "");

            for (int i = 1; i <= 12; i++) {
                model.addAttribute("checkBox" + i, Boolean.FALSE);
                model.addAttribute("scheduleContent" + i, "");
            }
            log.info("checkBox1 value: {}", model.getAttribute("checkBox1"));

        } catch (Exception e) {
            log.error("오류 발생: {}", e.getMessage());
            return "redirect:/login?error=unauthorized";
        }
        log.info("home 이동");

        return "home";
    }

    @GetMapping("/testPage")
    @PreAuthorize("isAuthenticated()")
    public String testPage() {
        log.info("testPage");

        return "testPage";
    }

}

 

home에서는 새로고침을 고려해 토큰을 검사하기 때문에 인증 문제로 login으로 이동하게끔 구성했다.

 

오류처리는 여기까지 했고 각 오류를 자바 스크립트와 spring에서 제공하는 에러페이지 핸들링을 통해서 처리하도록 구성했다.

 

4xx.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Jlaner</title>
</head>
<body>
<div>
    <p>인증 오류입니다. 로그인 페이지로 돌아가 주세요.</p>
    <a href="/login" class="button">로그인 페이지로 이동</a>
</div>
</body>
</html>

 

5xx.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <p>서버 오류입니다. 로그인 페이지로 돌아가 주세요.</p>
    <a href="/login" class="button">로그인 페이지로 이동</a>
</div>
</body>
</html>

 

400대 오류와 500대 오류를 처리하기 위한 페이지 구성으로 templates/error/ 경로에 html을 구성했다.

클라이언트 측 오류나 서버 측 오류는 페이지로 로그인  페이지로 넘가게 구성해 처리한다.

 

그렇다면 로그인 페이지로 넘어갔을 때 error로 파라미터 오류 처리한 부분도 알맞게 처리해야 한다. 이는 js로 처리했다.

function alertError() {
            const params = new URLSearchParams(window.location.search);
            const error = params.get('error');

            if (error) {
                let message = '';

                switch (error) {
                    case 'memberNotFound':
                        message = '회원 정보를 찾을 수 없습니다. 다시 로그인해주세요.';
                        break;
                    case 'invalidRefreshToken':
                        message = '유효하지 않은 리프레시 토큰입니다. 다시 로그인해주세요.';
                        break;
                    case 'missingRefreshToken':
                        message = '리프레시 토큰이 없습니다. 다시 로그인해주세요.';
                        break;
                    case 'unauthorized':
                        message = '인증되지 않은 요청입니다. 다시 로그인해주세요.';
                        break;
                }

                alert(message);
            }
}

// 페이지가 로드될 때 showAlertFromQuery 함수를 호출
window.onload = alertError;

 

로그인 페이지로 이동할 때 자바스크립트로 alert를 통해 처리하도록 했다.

이로써 오류는 모두 처리했지만 아직 더 해야 하는 부분이 하나 있다.

만약 post나 schedule을 작성하거나 작성 중에 불가피하게 login 페이지로 이동하거나 나가게 될때 작성중 데이터가 유실된다.

이는 사용자가 불편함을 크게 느낄 수 있는 부분이기 때문에 작성중 페이지를 나가거나 꺼지게 된다면 쿠키나 로컬 스토리지에 데이터를 저장하고 불러올 수 있도록 구성할 것이다.

// 페이지를 떠나기 전에 데이터를 로컬 스토리지에 저장
window.addEventListener('beforeunload', function(event) {
    // 텍스트 영역의 내용을 로컬 스토리지에 저장
    const contentData = document.getElementById('shared-textarea').value;
    localStorage.setItem('contentData', contentData);

    // 스케줄 영역의 각 항목을 로컬 스토리지에 저장
    for (let i = 1; i <= 12; i++) {
        const timeSlot = formatTimeSlot(i);
        const checkbox = document.getElementById(`check-${timeSlot}`).checked;
        const textarea = document.getElementById(`text-${timeSlot}`).value;

        // 각 항목을 개별적으로 저장
        localStorage.setItem(`check-${timeSlot}`, checkbox);
        localStorage.setItem(`text-${timeSlot}`, textarea);
    }
});

// 시간을 포맷하는 함수 (2시간 간격)
function formatTimeSlot(index) {
    const startHour = (index - 1) * 2;
    const endHour = startHour + 2;
    return `${String(startHour).padStart(2, '0')}-${String(endHour).padStart(2, '0')}`;
}

// 페이지 로드 시 로컬 스토리지에서 데이터를 복원
document.addEventListener('DOMContentLoaded', function() {
    // 복원 여부를 묻는 알림
    const restoreData = confirm("이전 데이터를 복원하시겠습니까?");

    if (restoreData) {
        // 텍스트 영역의 내용을 로컬 스토리지에서 복원
        const contentData = localStorage.getItem('contentData');
        if (contentData) {
            document.getElementById('shared-textarea').value = contentData;
        }

        // 스케줄 영역의 각 항목을 로컬 스토리지에서 복원
        for (let i = 1; i <= 12; i++) {
            const timeSlot = formatTimeSlot(i);
            const checkboxValue = localStorage.getItem(`check-${timeSlot}`);
            const textareaValue = localStorage.getItem(`text-${timeSlot}`);

            // 복원된 값이 있는 경우에만 해당 요소에 설정
            if (checkboxValue !== null) {
                document.getElementById(`check-${timeSlot}`).checked = (checkboxValue === 'true');
            }

            if (textareaValue !== null) {
                document.getElementById(`text-${timeSlot}`).value = textareaValue;
            }
        }
    }
});

 

 

 

위와 같이 구성했다. 처음에는 쿠키에 저장하는 js 코드를 작성했지만 쿠키에 저장되는 데이터가 크게 되면 저장되지 않을 수 있다.

4KB 이상이면 저장되지 않는다고 했다. 보통 글만 작성하기에 4KB를 넘기 어렵지만 서버 측 부담이 가는 것도 아니기 때문에 굳이 사용범위가 좁은 쿠키를 사용하기보다는 localstorage를 사용하는 것이 이점이 많겠다 생각했다. 

 

document.getElementById('schedule-form').addEventListener('submit', function(event) {
        event.preventDefault();  // 기본 폼 제출 방지
        saveScheduleData();  // savePostData 함수 호출

        localStorage.removeItem('contentData');
                for (let i = 1; i <= 12; i++) {
                    const timeSlot = formatTimeSlot(i);
                    localStorage.removeItem(`check-${timeSlot}`);
                    localStorage.removeItem(`text-${timeSlot}`);
                }
    });
    
    document.getElementById('post-form').addEventListener('submit', function(event) {
        event.preventDefault();  // 기본 폼 제출 방지
        savePostData();  // savePostData 함수 호출

        localStorage.removeItem('contentData');
                for (let i = 1; i <= 12; i++) {
                    const timeSlot = formatTimeSlot(i);
                    localStorage.removeItem(`check-${timeSlot}`);
                    localStorage.removeItem(`text-${timeSlot}`);
                }
    });

 

이후 게시물을 저장하거나 게시물을 불러오는 곳에서 LocalStorage의 데이터를 삭제해 주었다. 데이터를 저장하거나 불러올 때에는 저장된 데이터가 필요하지 않을 것이기 때문이다.

 

이로써 오류 페이지 구성과 예외 처리를 해주었다.

사실 여태 js를 사용한 적이 없어서 서버 측에서 처리만 했다 보니 조금 전과 같은 로컬 스토리지에 저장하는 것도 서버 코드로 하려고 했었다. 임시 저장을 만들어 데이터를 저장하고 불러오기로 저장된 데이터를 불러오는 방식을 생각했지만 지금처럼 쿠키나 로컬 스토리지를 사용하면 더 유연하고 서버에 부담되지 않는 코드를 작성할 수 있는 것 같아 만족했다.

 

 

다음 포스팅부터는 배포가 되겠다.

배포는 또 새롭게 하는 것도 있고 오래간만에 하는 것이니 공부도 더 하면서 도커, 깃, aws, nginx 개별적으로는 다 사용해 보았지만 통합적으로 배포 흐름으로 한 번에 사용해 본 적은 없기 때문에 뚝딱댈 수 있고 실수나 잘못된 정보가 있을 수 있으며, 자신은 없지만 열심히 해서 포스팅하도록 하겠다.