[React + Spring]연동 및 빌드 후 404에러

Inung_92·2024년 4월 3일
1

Spring

목록 보기
15/15
post-thumbnail

포스팅 목적

📖 스프링부트 + 리액트를 통합 빌드하여 연동된 웹 애플리케이션 프로젝트를 개발하는 도중 발생한 404 에러에 대한 원인 및 해결 방법에 대해 알아보자.

상황 설명

⚡️기술스택(버전)

  • SpringBoot : 3.1.8
  • React : 18.2.0
  • react-router-dom : 6.21.1
  • 빌드 : Gradle

⚡️기타 주요 설정

  • React Router : HashRouter 사용
  • JWT를 통한 로그인 인증을 위해 Interceptor 사용
  • SpringBoot contextPath : '/api'로 설정

⚡️에러 상황

개발이 어느정도 진행이 되고, 테스트를 수행하던 중 배포를 위한 빌드 테스트를 수행했다. 빌드는 Gradle을 통해 아래 스크립트를 이용해서 React를 먼저 빌드하고, SpringBoot의 빌드가 수행되도록 설정했다.(혹시 SpringBoot와 React 연동에 대해 모르신다면 여기를 먼저 확인하자.)

def frontendDir = "$projectDir/src/main/frontend"

sourceSets {
    main {
        resources {
            srcDirs = ["$projectDir/src/main/resources"]
        }
    }
}

processResources { dependsOn "copyReactBuildFiles" }

task installReact(type: Exec) {
    workingDir "$frontendDir"
    inputs.dir "$frontendDir"
    group = BasePlugin.BUILD_GROUP
    if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
        commandLine "npm.cmd", "audit", "fix"
        commandLine 'npm.cmd', 'install'
    } else {
        commandLine "npm", "audit", "fix" commandLine 'npm', 'install'
    }
}

task buildReact(type: Exec) {
    dependsOn "installReact"
    workingDir "$frontendDir"
    inputs.dir "$frontendDir"
    group = BasePlugin.BUILD_GROUP
    if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
        commandLine "npm.cmd", "run-script", "build"
    } else {
        commandLine "npm", "run-script", "build"
    }
}

task copyReactBuildFiles(type: Copy) {
    dependsOn "buildReact"
    from "$frontendDir/build"
    into "$projectDir/src/main/resources/static"
}

빌드가 성공하고, http://localhost:8080/으로 접속했는데 404(Not Found) 에러가 발생했다. Router도 설정했고, Interceptor에서 '/**'를 기준으로 preHandle이 동작하도록 했는데 무엇이 문제인지 몰라서 원인을 찾아나가기 시작했다.

먼저, 원인에 대해 설명하기 전에 아래 사항을 체크해보길 바란다.

  • interceptor를 사용하고 있는지.
  • SpringBoot의 contextPath를 설정했는지.
  • 빌드된 React의 결과물이 resources/static 하위 폴더에 올바르게 들어와 있는지.

위 사항들이 해당 된다면 아래 원인 및 해결 방법을 알아보고, 해당 사항이 없다면 다른 원인일 수 있으니 다른 글을 참고하시는 걸 추천한다.


원인

⚡️SpringBoot contextPath

📖 contextPath는 애플리케이션 접근을 위한 기본 경로이자 DispatcherServlet이 동작하는 기준경로

애플리케이션의 기준 경로를 잡아주기 위한 이유 때문에 contextPath'/api'처럼 커스텀으로 설정했다면 http://localhost:8080/ 이 아니라 http://localhost:8080/api 로 접속을 해야 index.html이 로드되어 정상적인 페이지를 볼 수 있다.

# 내 설정
server:
	servlet:
    	contextPath: '/api'

하지만 나는 React에서 프록시로 설정한 URL 매핑을 컨트롤러마다 적용하기가 귀찮아서 contextPath'/api'를 설정한 것이 문제였다. contextPath는 애플리케이션의 기본 경로이기 때문에 당연히 '/api'로 접속을해야 기본 페이지가 보이는 것인데 설정을 해놓고도 아무런 생각없이 '/'로만 접속을 시도하고 있었던 것이다.

하지만 이 설정은 contextPath에 대한 이해와 해당 경로로 접속을하면 크게 문제가 되지는 않는 원인이다. 그렇다면 왜 나는 이 부분을 원인으로 선택했는가?

⚡️Interceptor 사용

📖 인터셉터는 Spring 컨텍스트 내에서 동작하며, Dispatcher 컨트롤러를 호출하기 전, 후와 요청이 완료된 이후의 작업을 지정하여 수행할 수 있도록 하는 코드 조각

필터와는 다르게 스프링 컨텍스트 내에서 동작하여 스프링의 영향을 받으며, 동작할 대상 URL과 제외 URL을 설정하여 작업을 수행할 수 있다.

// WebConfig.java 일부
...
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
        		.addPathPatterns("/**", "/**")
                .excludePathPatterns("/login");
    }
...

오류가 날 당시에는 위 코드와 같이 '/'를 대상 패턴으로 적용했고, contextPath'/api'로 설정되어있었다. 이 상황에서 빌드를 하면 http://localhost:8080/api로 접속해야만 정상적인 페이지가 로드된다.

하지만 '/'로 접속해서 404 에러를 마주하고, '/api'로 접속해서 unauthorized 에러를 마주하니 환장할 노릇이었다. 설정했던 부분을 까먹고, 혼자 삽질을 하고 있었던 것인데...

여기서 contextPath의 존재를 알아채고, '/'로 변경 후 접속했더니 unauthorized 에러가 나는 것을 확인하였고, 이 부분에서 힌트를 얻었다.


해결

이게 혼자서 무슨 삽질인가 싶지만 해결된 후에 깨달은 정상적인 설정에 대해서 이야기해보겠다.

⚡️contextPath

사실 이 부분에 대한 설정은 자유롭게 하면된다. 다만, 나처럼 바보같이 까먹지만 않으면 되고, contextPath는 애플리케이션의 기본 경로가 된다는 점을 인식하고, 설정하자.

⚡️Interceptor

사용하지 않는 사람들은 상관이 없지만 설정해서 사용하고자 한다면 기본적으로 적용할 대상 URL은 contextPath 경로가 최초 진입점이 되도록 설정하고, 해당 기본 경로를 포함하여 제외할 URL을 지정하자.

아래 코드로 예시를 들어보겠다.

// WebConfig.java 일부
...
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)
        		// contextPath = '/babo'
        		.addPathPatterns("/babo/**", "/babo_file/**")
                .excludePathPatterns("/babo/login"); // login만 제외
    }
...

여기서 제외할 URL을 '/login'으로 설정했다면 개발 단계에서는 거의 무리 없이 동작할 것이다. 하지만 통합 빌드를 수행한 후에 실행시키면 react의 개발 서버는 동작하지 않고, 스프링이 모든 동작을 핸들링 하기 때문에 contextPath에 설정된 기본 경로로 진입해야 react가 빌드된 index.html이 정상적으로 작동한다.

어떻게 보면 정말 간단하게 해결될 문제였다. 물론 3시간 정도로 비교적 양호(?)하게 해결했지만 스프링부트와 리액트를 연동하고, 통합 빌드하시는 분들이 나와 같은 실수를 하지 않기를 바란다.

profile
서핑하는 개발자🏄🏽

0개의 댓글