2025년 11월 21일 금요일 (120일차)

Jeonghoon·2025년 11월 21일

jeonghoon's Study

목록 보기
123/128

📝 개발 학습 노트: AWS S3 & Spring Boot + React 연동

☁️ [AWS] S3 (Simple Storage Service)

1. 정의 📖

"인터넷을 위한 확장 가능하고 안전하며, 내구성이 뛰어난 객체 스토리지 서비스"

AWS S3는 이미지, 동영상, 백업 데이터 등 모든 유형의 데이터를 저장하고 검색할 수 있도록 설계되었습니다. 웹사이트 호스팅, 백업, 아카이브, 엔터프라이즈 애플리케이션 등 다양한 용도로 사용됩니다.

2. 목적 🎯

  • 인터넷 환경(Web)에서 모든 유형의 데이터를 안전하고 확장 가능하며 내구성 있게 저장 및 검색할 수 있도록 지원합니다.

3. 주요 용어 정리 📚

용어 (Term)설명 (Description)비유 (Analogy)
버킷 (Bucket)• S3에 데이터를 저장하기 위한 최상위 컨테이너
• 전 세계적으로 고유한 이름을 가져야 함
• 객체들을 그룹화하여 관리하는 역할
🗂️ 윈도우의 'C 드라이브' 또는 '최상위 폴더'
객체 (Object)• S3에 저장되는 실제 데이터 파일
• 파일 데이터 + 메타데이터(설명 정보)를 포함하는 기본 저장 단위
📄 실제 파일 (사진, 문서 등)
키 (Key)• 버킷 내에서 각 객체를 고유하게 식별하는 이름
• 디렉토리 구조처럼 보이지만 실제로는 긴 이름의 문자열(파일 경로 역할)
🔑 파일의 전체 경로 (예: /images/logo.png)

☕ [Spring Boot] + ⚛️ [React] 통합 빌드 및 배포

스프링 부트 프로젝트 안에 리액트 프로젝트를 포함하여, 하나의 JAR 파일로 배포하기 위한 설정입니다.

1. Gradle 설정 (build.gradle) 🐘

리액트의 빌드 과정을 스프링 부트의 빌드 과정에 포함시키는 스크립트입니다. node가 설치되어 있어야 하며, 플러그인을 통해 자동화합니다.

💡 프로세스 순서:
1. npm install (의존성 설치)
2. npm run build (리액트 앱 빌드)
3. 빌드된 결과물(dist 등)을 스프링의 resources/static으로 복사

// 1. npm install 실행 Task (React 의존성 설치)
task npmInstallReact(type: NpmTask) {
    description = "Install Node.js dependencies from package.json"
    // ⚠️ 중요: 실제 리액트 프로젝트가 위치한 폴더명으로 변경해야 합니다.
    workingDir = file("${project.projectDir}/src/main/리액트폴더명") 
    args = ['install']
    
    // 변경 사항 감지를 위한 입력/출력 파일 지정 (빌드 최적화)
    inputs.file("src/main/리액트폴더명/package.json")
    inputs.file("src/main/리액트폴더명/package-lock.json") 
    outputs.dir("src/main/리액트폴더명/node_modules")
}

// 2. npm run build 실행 Task (React 앱 빌드)
task npmBuildReact(type: NpmTask) {
    description = "Build the React application using npm run build"
    dependsOn npmInstallReact // install이 먼저 실행되도록 의존성 설정
    workingDir = file("${project.projectDir}/src/main/리액트폴더명")
    args = ['run', 'build']
    
    // 소스 코드가 변경되었을 때만 재빌드하도록 설정
    inputs.dir("src/main/리액트폴더명/src")
    inputs.dir("src/main/리액트폴더명/public")
    inputs.file("src/main/리액트폴더명/package.json")
    
    // ⚠️ 중요: Vite 사용 시 'dist', CRA 사용 시 'build' 폴더 확인 필수!
    outputs.dir("src/main/리액트폴더명/dist")
}

// 3. 리액트 빌드 결과물을 스프링의 resources 폴더로 복사
// -> 스프링 서버(8080) 하나로 리액트 화면까지 띄우기 위함
task copyReactBuild(type: Copy) {
    description = "Copy React build output to Spring Boot static resources"
    dependsOn npmBuildReact // 빌드가 완료된 후 복사 실행
    
    // 원본 위치 (리액트 빌드 폴더)
    from "${project.projectDir}/src/main/리액트폴더명/dist"
    // 목적지 위치 (스프링 정적 리소스 폴더)
    into "${project.buildDir}/resources/main/static"
}

2. SPA 경로가 같을 수 있기에 Fliter를 통해 필터링

@Component // Spring Bean으로 등록하여 필터 체인에 자동으로 추가되도록 함
public class SpaWebFilter implements Filter {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    // 필터링에서 제외할 경로 시작 패턴 목록 (API 경로, 정적 리소스 등)
    // 실제 프로젝트의 API 경로 접두사, 정적 리소스 경로에 맞게 수정해야 합니다.
    private final List<String> EXCLUDE_PATHS = Arrays.asList(
            "/api/",       // 모든 API 호출
            "/static/",    // 정적 리소스 (Create React App 기본)
            "/assets/"    // 정적 리소스 (Vite 기본)
            // 다른 백엔드 전용 경로 추가 가능
    );

    // 필터링에서 제외할 특정 파일 확장자 또는 파일명
    // точка(.)를 포함하지만 SPA 라우팅으로 처리해야 하는 경우는 여기에 추가하지 않음
    private final List<String> EXCLUDE_FILES = Arrays.asList(
            "/favicon.ico",
            "/robots.txt"
            // 다른 특정 정적 파일 추가 가능
    );


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        // 1. HTTP 요청이 들어오면 해당 URL 들어오기
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String path = httpRequest.getRequestURI();

        // 2. 백엔드 내에 존재하지 않는 경로는 리액트로 이동
        if (shouldForwardToSpa(path)) {
            logger.debug("SPA WebFilter: Forwarding request to /index.html for path: {}", path);
            RequestDispatcher dispatcher = request.getRequestDispatcher("/index.html");
            dispatcher.forward(request, response);
            return; // 포워딩 후 필터 체인 중단
        }

        // API 호출이나 정적 리소스 요청 등은 그대로 필터 체인 계속 진행
        chain.doFilter(request, response);
    }

    private boolean shouldForwardToSpa(String path) {
        // 1. 명시적인 제외 파일/경로인지 확인
        if (EXCLUDE_FILES.stream().anyMatch(p -> path.equals(p))) {
            return false;
        }
        // 2. 제외 경로 패턴으로 시작하는지 확인 (API, static 등)
        if (EXCLUDE_PATHS.stream().anyMatch(p -> path.startsWith(p))) {
            return false;
        }
        // 3. 경로에 .(점)이 포함되어 파일 확장자를 가질 가능성이 높은 경우 제외 (단순 휴리스틱)
        if (path.contains(".") && path.lastIndexOf('.') > path.lastIndexOf('/')) {
            // 예외: 점을 포함하지만 SPA 라우트인 경우 (예: /profile/user.name) 여기에 로직 추가 가능
            return false; // 일단 확장자 있는 경로는 제외
        }

        // 위 조건에 모두 해당하지 않으면 SPA 라우팅 대상일 가능성이 높음
        return true;
    }

    // init, destroy 메서드는 기본 구현 사용 가능
}

0개의 댓글