Spring Boot + React : 서버의 URL과 리액트 앱 URL을 구분하여 처리하기 (React 앱의 엔트리 포인트 설정방법)

들어가며

 이전에 작성한 포스팅에서 Spring Boot + React의 조합으로 프로젝트를 생성하는 방법을 정리한 적이있다.

 

 

Springboot + react 조합으로 프로젝트 설정하기

개발환경 - IDLE : IntelliJ - Java : OracleJDK 17 - node.js : 16.17 version - gradle 프로젝트 - mariaDB 1. 생성한 Springboot 프로젝트 의 src/main 디렉토리에 React Project생성 npx create-react-app 프로젝트명 이때 프로젝트명

data-study-clip.tistory.com

 

 이때 Build.gradle을 아래와 같이 작성하여 리액트 앱이 빌드되면 빌드된 결과값을 Spring Boot 프로젝트의 static 디렉토리 밑에 복사되도록 설정해주어 react로 작성된 프론트 작업 결과물을 spring에서 정적자원으로 관리하게끔 하였다.

 

//  React 앱의 빌드 결과물을 Spring Boot 프로젝트의 resources/static 디렉토리로 복사
//  React 앱의 빌드 결과물이 Spring Boot 프로젝트 내부에 적절히 배치하기 위함
task copyReactBuildFiles(type: Copy) {
    dependsOn "buildReact"
    from "$frontendDir/build"
    into "$projectDir/src/main/resources/static"
}

 

 위와 같은 방법으로 Spring boot + React 조합의 프로젝트를 생성했다면 리액트의

Link 컴포넌트를 통해서 페이지 라우팅을 하면 해당 URL은 리액트 앱이 실행되고 있는 도메인과 포트에 기반한다.

 

예를 들어 리액트가 localhost:3000에서 실행 중이라면, Link 컴포넌트를 통해 "/about" 페이지로 이동하게 되면 URL은 "localhost:3000/about"이다. 이때 실제로 요청은 서버에 전송되지 않고 리액트 앱 내부에서 처리(라우팅)된다.

 

일전에 리액트앱의 package-json의 프록시 설정은 API 요청과 같이 서버에 데이터를 요청할 때만 사용되는 것이다.

즉 proxy 설정은 페이지 라우팅 등의 클라이언트 사이드 동작에는 영향을 주지 않는다.

 

 그리고 Link 컴포넌트를 통해 "/about" 페이지로 이동하게 되면 URL은 "localhost:3000/about"으로 변경 된 이후 사용자가 새로고침을 하면 그 새로고침은 localhost:8080/about 으로 요청을 보내게 되고 만약 Spring 서버에서  /about에 대한 컨트롤러가 없다면 404가 발생한다.

 

 

 이처럼 클라이언트 사이드에서 사용하는 URL과 서버에 요청을 보낼 때 사용하는 URL을 구분하여 처리하도록

리액트 앱에서 Spring 서버로 요청을 보내는 URL인 경우 앞에 "/api/" 를 붙인 다는 규칙을 만드는 식으로 

별도의 URL 설계도 필요하고 서버에서 React App의 엔트리 포인트를 설정도 해주어야 한다.

 

(참고) 엔트리 포인트(Entry Point)란?

프로그램이 실행되기 시작하는 위치, 즉 프로그램의 시작점을 엔트리 포인트(Entry Point)라고한다.

 

우선 API 설계부터 알아보도록 하자.

 

1. API 설계

- Controller

@RestController
@RequestMapping("/api")
public class GameNewsController {

    private final NewsPageRepositoryService newsPageRepositoryService;

    @Autowired
    public GameNewsController(NewsPageRepositoryService newsPageRepositoryService) {
        this.newsPageRepositoryService = newsPageRepositoryService;
    }

    @GetMapping("/news/list")
    public ResponseEntity<Map<String, Object>> getNewsList
            (@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) {

        PagingDTO pagingDTO = new PagingDTO();
        pagingDTO.setPage( (page-1)*10 );
        pagingDTO.setSize(size);

        List<NewsPagesDTO> newsList = newsPageRepositoryService.getNewsList(pagingDTO);
        int totalCount = newsPageRepositoryService.getTotalNewsDataCount();
        pagingDTO.setTotal_count(totalCount);

        Map<String, Object> response = new HashMap<>();

        response.put("paging", pagingDTO);
        response.put("newsList", newsList);
        return new ResponseEntity<>(response, HttpStatus.OK);

    }

    @GetMapping("/news/detail")
    public ResponseEntity<NewsPagesDTO> getNewsPage(@RequestParam String news_id){
        NewsPagesDTO result = newsPageRepositoryService.getNewsPage(news_id);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

}

 

 위처럼 서버의 Controller에서 리액트 앱에서 요는 요청들을 앞에 /api 로 구분한다.

 

- React에서 API 호출

function NewsListPage() {
    const [newsList, setNewsList] = useState([]);
    const [page, setPage] = useState(1);
    const [totalCount, setTotalCount] = useState(0);
    const [buttonRange, setButtonRange] = useState(DEFAULT_BUTTON_RANGE);

    useEffect(() => {
        fetchNewsList();
        updateButtonRange();
    }, [page]);

    async function fetchNewsList() {
        try {
            const response = await axios.get('/api/news/list', { params: { page: page, size: PAGE_SIZE } });
            if (response.status === 200) {
                setNewsList(response.data.newsList);
                setTotalCount(response.data.paging.total_count);
            }
        } catch (error) {
            console.error('Failed to fetch news list:', error);
        }
    }

당연히 리액트에서는 호출할 때 /api 를 붙인다.

이렇게 하면 서버에서 React에서 들어오는 API 호출 요청을 구분지을 수 있다.

 

그런데 아까 말했던 것처럼 React App에서 라우팅된 페이지에서 사용자가 새로고침을 할 경우

해당 URL은 리액트 앱에서만 처리하는 URL인데도 불구하고 서버쪽으로 요청이 전송되니 404 에러가 발생할 수 있다.

 

따라서 이를 해결하기 위해서 아래와 같이 Spring에서 WebMvcConfigurer 인터페이스를 구현하여

React App의 엔트리 포인트를 제공하도록 설정해주어  React Router가 클라이언트 사이드에서 렌더링 될 수 있도록 한다.

 

 

2. WebMvcConfigure 구현을 통한 React App 엔트리 포인트 설정하기

package com.example.happyusf.Configure;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;

import java.io.IOException;

/**
 * @Explain : Spring Boot에서 항상 index.html(React 앱의 엔트리 포인트)을 제공하도록 설정하여
 *          React Router가 클라이언트 사이드에서 적절하게 컴포넌트를 렌더링 할 수 있도록한다.
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .resourceChain(true)
                .addResolver(new PathResourceResolver() {
                    @Override
                    protected Resource getResource(String resourcePath,
                                                   Resource location) throws IOException, IOException {
                        Resource requestedResource = location.createRelative(resourcePath);

                        return requestedResource.exists() && requestedResource.isReadable() ?
                                requestedResource : new ClassPathResource("/static/index.html");
                    }
                });
    }
}

 WebMvcConfigure 인터페이스는 Spring MVC 설정을 개발자가 커스터마이징할 수 있도록 해주는 인터페이스이다.

인터페이스의 메소드를 정리하면 아래와 같다.

 

- addResourceHandlers() : 정적 리소스 처리를 위한 핸들러를 등록하는 메소드이다.

- addResourceLocations() : 요청된 리소스가 위치하는 곳을 지정하는 메소드.

- getResource() : 주어진 resourcePath에 해당하는 리소스를 반환

 

위 코드를 보면 Spring Boot 서버가 받은 모든 HTTP 요청은 static 디렉토리에서 해당 파일이 있는지 먼저 확인하고

파일이 없거나 읽지 못한 경우 항상 /static/index.html" 자원을 반환하게 된다.

이는 곧 React 앱의 엔트리 포인트를 제공하게 되는 것이다.

 

위와 같이 작성하면 Spring Boot에서는 모든 알려지지 않은 경로에 대한 요청을 "index.html"(React 앱의 엔트리 포인트)로 리다이렉션하게된다.

 

이렇게 하면 React Router가 해당 경로에 대한 라우팅을 처리할 수 있게 된다.