[Project] React / SpringBoot - Excel file download

이슬기·2024년 1월 21일
0

project

목록 보기
14/42

이전 게시물에서 SpringBoot로 엑셀 파일을 다운 받는 로직을 처리하였다.
이제 리액트와 연결하여 화면에 출력이 되도록 처리하고자 한다.

리액트에서 엑셀 다운로드를 시도했을 때 코드

스프링에서 엑셀 다운로드를 완성하기 전 리액트에서도 시도해봤던 코드이다. 하지만 제대로 실행되지 않았고 스프링에서 구현했기 때문에 리액트는 이와 연결해서 다운로드가 되도록 코드를 수정하고자 한다.

const excelDown = async () => {
            console.log("excelDown 호출");
            // 서버에서 엑셀 다운로드를 요청
            const res = await excelDownDB({ emps });
            console.log(res);
            setExcels(res.data);

            // Blob 데이터로부터 파일을 생성하고 다운로드
            const blob = new Blob([generateExcel(emps)], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
            const link = document.createElement('a');
            link.href = window.URL.createObjectURL(blob);
            link.download = 'employees.xlsx';
            link.click();
        };

        const generateExcel = (data) => {
            const workbook = new ExcelJS.Workbook();
            const sheet = workbook.addWorksheet('Employee List');

            // 헤더 추가
            sheet.addRow(['사원번호', '이름', '성별', '전화번호']);

            // 데이터 추가
            data.forEach(emp => {
                sheet.addRow([emp.e_code, emp.e_name, emp.e_gender, emp.e_phone]);
            });

            // 엑셀 파일을 바이트 배열로 변환
            return workbook.xlsx.writeBuffer().then(buffer => buffer);
        };

스프링부트에서 엑셀 파일 다운로드 처리 후 리액트 코드

리액트에서는 파일 다운로드를 위해 브라우저가 직접 다운로드를 처리하므로 리액트에서는 특별한 처리가 필요하지 않다. 엑셀 파일 다운로드 요청을 보내고 서버에서는 파일을 생성하여 반환하면 브라우저에서 파일 다운로드 창이 뜨게 된다.

👇 excelDown 함수 처리 - 엑셀 다운로드 설정

const excelDown = async () => {
    console.log("excelDown 호출");
    try {
        // 서버에서 엑셀 다운로드 요청
        const res = await excelDownDB({});
        console.log(res);
        // 리턴받은 파일 데이터를 가지고 다운로드 창을 열어줌
        const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'employee_list.xlsx';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        window.URL.revokeObjectURL(url);
    } catch (error) {
        console.error('Error downloading Excel file:', error);
    }
};

스프링부트 - Controller의 EmpVO evo도 제외

@GetMapping("excelDown")
public ResponseEntity<Resource> excelDown(EmpVO eVO)

👇

@GetMapping("excelDown")
public ResponseEntity<Resource> excelDown()

에러 발생 - 파일 읽기 오류

주소창에 바로 테스트를 했을 때는 정상적인 엑셀 파일이 다운로드 되어 사원목록이 보였지만 리액트를 통해 Excel Down버튼을 누르면 아래 그림과 같이 파일 읽기 오류. 지원하지 않는 파일 형식이거나 파일이 손상되었습니다.라는 문구가 뜨며 파일 내용이 비어있다.

문제해결과정

  1. 현재 서버 측 코드에서는 XSSFWorkbook을 사용하여 엑셀 파일을 생성하고 있습니다. 그런데 XSSFWorkbook은 .xlsx 확장자를 가진 엑셀 파일을 생성합니다. 클라이언트에서 해당 파일을 열 때, 브라우저는 확장자를 보고 엑셀로 올바르게 해석합니다.

만약 파일이 비어있다면, 엑셀 생성 부분에서 문제가 발생한 것일 수 있습니다. 빈 Sheet을 생성하고 각 셀에 데이터를 추가하는 등의 작업이 정상적으로 이루어졌는지 확인해야 합니다. 또한, 엑셀 파일이 다운로드될 때 데이터를 올바르게 전달하고 있는지도 확인이 필요합니다.

다음은 조금 더 안전한 방법으로 엑셀 파일을 생성하고 다운로드하는 코드입니다.

@GetMapping("excelDown")
public ResponseEntity<Resource> excelDown() {
    try {
        // 엑셀 파일 생성
        Workbook wb = new SXSSFWorkbook();
        Sheet sheet = wb.createSheet("사원 목록");
        Row row;
        Cell cell;
        int rowNo = 0;

        // 테이블 헤더용 스타일
        CellStyle headStyle = wb.createCellStyle();
        // ... (이하 코드 생략)

        // 데이터용 경계 스타일 테두리만 지정
        CellStyle bodyStyle = wb.createCellStyle();
        // ... (이하 코드 생략)

        // 헤더 생성
        row = sheet.createRow(rowNo++);
        cell = row.createCell(0);
        cell.setCellStyle(headStyle);
        cell.setCellValue("사원번호");
        // ... (이하 코드 생략)

        // 데이터 생성
        // ... (이하 코드 생략)

        // 엑셀 파일을 ByteArray로 변환
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        wb.write(bos);
        wb.close();

        // ByteArray를 Resource로 변환
        ByteArrayResource resource = new ByteArrayResource(bos.toByteArray());

        // 파일 이름에 공백이 있는 경우 URL 인코딩 필요
        String filename = "employee_list.xlsx";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        headers.setContentDispositionFormData("attachment", URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()));
        headers.setContentLength(bos.size());

        return ResponseEntity.ok()
                .headers(headers)
                .contentLength(bos.size())
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(resource);

    } catch (IOException e) {
        logger.error("Error occurred while creating Excel file", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

위 코드에서 SXSSFWorkbook을 사용하여 엑셀 파일을 생성하였습니다. SXSSFWorkbook는 대용량 데이터에 대한 처리를 개선한 버전으로, 성능 및 안정성이 향상되어 있습니다. 또한, 파일 이름에 공백이 있는 경우에도 정상적으로 처리하기 위해 URLEncoder.encode를 사용하여 파일명을 인코딩하였습니다.

  1. 문제가 지속되고 있다면 브라우저가 제대로 파일을 해석하지 못하는 문제가 될 수 있습니다. 여러 가지 원인이 있을 수 있습니다. 몇 가지 가능한 해결책을 제시해 보겠습니다.

  2. 파일 확장자 확인: 다운로드 받은 파일의 확장자가 .xlsx로 되어 있는지 확인해주세요. 서버에서는 employee_list.xlsx로 파일명을 지정하고 있으므로, 브라우저에서도 이와 일치하는 확장자를 기대할 것입니다.

  3. 파일명 인코딩 문제: 파일명을 인코딩할 때 문제가 발생할 수 있습니다. URLEncoder.encode 대신에 아래와 같이 변경해 보세요.

headers.setContentDispositionFormData("attachment", "filename*=UTF-8''" + URLEncoder.encode(filename, StandardCharsets.UTF_8.toString()));
headers.setContentDispositionFormData("attachment", "employee_list.xlsx");
  1. 파일 내용 확인: 서버에서 생성한 파일이 정상적으로 데이터를 담고 있는지 확인해보세요. 서버에서 생성한 파일을 로컬에 저장한 후, 열어서 내용을 확인해보세요. 이 작업을 통해 서버에서 파일을 제대로 생성하는지 여부를 확인할 수 있습니다.

  2. 파일 다운로드 방식 변경: 클라이언트 측에서 파일을 다운로드할 때 사용하는 방식을 변경해 보세요.

const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'employee_list.xlsx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);

위 코드의 type을 빈 문자열로 변경하여 시도해 보세요.

  1. 다른 브라우저 시도: 다른 브라우저에서도 같은 문제가 발생하는지 확인해보세요. 브라우저의 동작 차이로 인해 문제가 발생할 수 있습니다.

  2. 문제가 계속해서 발생한다면 브라우저에서 파일을 제대로 해석하지 못하는 문제일 수 있습니다. 브라우저에서 Blob 객체를 생성할 때 파일의 MIME 타입을 추론하는데, 이 과정에서 문제가 발생하는 것으로 보입니다.

다음과 같이 코드를 수정하여 시도해 보세요. 여기서 application/octet-stream은 일반적인 다운로드 MIME 타입입니다.

const blob = new Blob([res.data], { type: 'application/octet-stream' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'employee_list.xlsx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);

이러한 변경이 문제를 해결하지 못한다면, 브라우저 자체의 문제일 수 있습니다. 다른 브라우저에서 시도해보거나, 동일한 브라우저에서 다른 컴퓨터에서 테스트해보는 것도 도움이 될 수 있습니다. 브라우저 버전도 최신인지 확인해보세요.

또한, 서버에서 생성한 파일의 내용을 확인하여 파일이 제대로 생성되었는지 확인할 수 있습니다. 생성된 엑셀 파일을 다운로드하여 로컬에서 열어보고 내용을 확인해보세요.

  1. 주소창에 직접 주소를 입력하여 로컬에서 파일을 열어보면 정상적으로 파일을 열어볼 수 있다.
    개발자 센터 > network에서도 정상적으로 처리가 되었음을 확일할 수 있다.

  2. CORS 설정 확인: 브라우저에서 서버로의 요청이 CORS(Cross-Origin Resource Sharing)에 의해 차단되는지 확인합니다. 서버와 클라이언트 간의 도메인이 다를 경우 CORS 문제가 발생할 수 있습니다.

최종해결

리액트에서 파일 다운로드에 문제가 발생하는 경우, 다른 방법으로 시도해 볼 수 있는 것이 있습니다. 대신에 서버에서 직접 파일을 반환하는 것이 아니라, 클라이언트에서 먼저 데이터를 받아와서 파일로 저장하는 방식을 사용해 보겠습니다.

먼저, 서버 측에서는 excelDown 엔드포인트에서 엑셀 데이터를 반환하는 대신에 JSON 형태로 데이터를 반환하도록 변경합니다.

@GetMapping("excelDown")
public ResponseEntity<List<EmpVO>> excelDown() {
    try {
        logger.info("excelDown");

        // 엑셀 다운로드를 위한 데이터 조회
        List<EmpVO> eList = empLogic.excelDown();

        return ResponseEntity.ok().body(eList);
    } catch (Exception e) {
        logger.error("Error occurred while fetching Excel data", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

그런 다음, 클라이언트에서는 이 데이터를 받아와서 브라우저에서 파일로 저장하는 방식으로 처리합니다.

const excelDown = async () => {
    try {
        console.log("excelDown 호출");

        // 서버에서 엑셀 다운로드 요청
        const res = await excelDownDB();
        const excelData = res.data;

        // 데이터를 JSON 형태로 받아옴
        const wb = XLSX.utils.book_new();
        const ws = XLSX.utils.json_to_sheet(excelData);
        XLSX.utils.book_append_sheet(wb, ws, "사원 목록");

        // 파일로 저장
        XLSX.writeFile(wb, "employee_list.xlsx");
    } catch (error) {
        console.error("Error occurred while downloading Excel", error);
    }
};

위 코드에서는 excelDownDB 함수로 서버에서 엑셀 데이터를 받아온 다음, XLSX 라이브러리를 사용하여 데이터를 엑셀 파일로 변환하고 브라우저에서 파일로 저장하는 방식으로 처리합니다.

위 방법을 사용하기 위해서는 install이 필요하다

npm install xlsx

이후 import를 했지만

import XLSX from 'xlsx';

이런 에러가 발생했다

ERROR in ./src/lsg/component/EmpListAll.jsx 74:17-36
export 'default' (imported as 'XLSX') was not found in 'xlsx' (possible exports: CFB, SSF, parse_xlscfb, parse_zip, read, readFile, readFileSync, set_cptable, set_fs, stream, utils, version, write, writeFile, writeFileAsync, writeFileSync, writeFileXLSX, writeXLSX)

이 오류는 xlsx 모듈에서 XLSX의 기본 내보내기(default export)를 찾을 수 없다는 것을 나타냅니다. xlsx 모듈에서는 XLSX를 명시적으로 가져와야 합니다.

이를 해결하려면 EmpListAll.jsx 파일의 상단에서 XLSX를 다음과 같이 가져와야 한다.

import * as XLSX from 'xlsx';

xlsx 라이브러를 사용하여 최종적으로 문제를 해결하였다.

SpringBoot와 React의 결합

스프링 부트와 리액트를 결합하여 사용하는 것은 매우 일반적인 개발 방식입니다. 이러한 구성은 백엔드와 프론트엔드 간의 분리된 역할을 제공하며, 각각의 기술 스택에 맞게 특화된 업무를 수행할 수 있습니다.

일반적인 웹 애플리케이션 아키텍처는 다음과 같습니다:

  1. 백엔드 (스프링 부트):
    데이터 처리 및 로직 처리 담당.
    RESTful API 또는 GraphQL을 통해 프론트엔드와 통신.
    데이터베이스와의 상호작용.

  2. 프론트엔드 (리액트):
    사용자 인터페이스(UI) 구현.
    사용자와의 상호작용 담당.
    백엔드에서 제공하는 API를 호출하여 데이터 가져오기.
    이러한 구조는 효율적인 개발을 위해 백엔드와 프론트엔드를 독립적으로 개발하고 테스트할 수 있도록 해줍니다. 각각의 엔드포인트(API)를 정의하여 백엔드와 프론트엔드가 서로 독립적으로 작동할 수 있도록 하는 것이 일반적입니다.

또한, 리액트에서 백엔드 API를 호출할 때 CORS (Cross-Origin Resource Sharing) 정책에 주의해야 하며, 백엔드에서는 보안상의 이유로 필요한 헤더를 설정해주어야 합니다. 이는 백엔드의 @CrossOrigin 어노테이션 등을 사용하여 처리할 수 있습니다.

해결방법에 대한 통찰

클라이언트에서 파일 다운로드를 위해 서버에서 데이터를 받아와서 브라우저에서 파일로 저장하는 방식은 일반적인 방법 중 하나입니다. 이러한 방식은 다음과 같은 장점을 갖습니다:

  1. 더 좋은 유저 경험: 사용자는 파일이 생성되기를 기다릴 필요 없이 빠르게 요청에 응답받아 파일을 다운로드할 수 있습니다.

  2. 동적 파일 생성: 클라이언트에서는 서버로부터 받은 데이터를 가공하여 원하는 파일 형식으로 저장할 수 있습니다. 이는 서버에서 파일을 미리 생성하는 방식과는 달리 동적으로 파일을 생성할 수 있는 유연성을 제공합니다.

  3. 간단한 서버 구현: 서버에서는 파일을 생성하고 저장하는 복잡한 과정 없이 데이터를 JSON으로 반환하면 됩니다.

다만, 이러한 방식의 주의할 점은 클라이언트에서 파일을 생성하고 저장할 때 브라우저의 보안 정책에 따라 제약이 있을 수 있다는 것입니다. 때로는 특정 브라우저에서는 이러한 방식이 작동하지 않을 수 있습니다. 따라서 브라우저 호환성을 고려하여 개발하는 것이 중요합니다.

코드 예제

서버 측과 클라이언트 측에서의 간단한 코드 예제

👇 서버 (Spring Boot)

@RestController
@RequestMapping("/emp")
public class EmpController {

    @Autowired
    private EmpLogic empLogic;

    @GetMapping("/excelDown")
    public ResponseEntity<List<EmpVO>> excelDown() {
        try {
            // 엑셀 다운로드를 위한 데이터 조회
            List<EmpVO> eList = empLogic.excelDown();

            // JSON 형태로 데이터 반환
            return ResponseEntity.ok(eList);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
}

👇 클라이언트 (React)

import React, { useEffect, useState } from 'react';
import { excelDownDB } from "../service/dbLogic";

const EmpListAll = () => {
    const [excels, setExcels] = useState([]);

    const excelDown = async () => {
        try {
            // 서버에서 엑셀 다운로드 요청
            const res = await excelDownDB();
            const excelData = res.data;

            // 데이터를 JSON 형태로 받아옴
            const jsonData = JSON.stringify(excelData);

            // 파일로 저장
            const blob = new Blob([jsonData], { type: 'application/json' });
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'employee_list.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            window.URL.revokeObjectURL(url);
        } catch (error) {
            console.error("Error occurred while downloading Excel", error);
        }
    };

    return (
        <>
            {/* 나머지 컴포넌트 및 UI 구성 */}
            <button onClick={excelDown}>Download Excel</button>
        </>
    );
};

export default EmpListAll;

이 코드 예제에서는 서버에서 엑셀 데이터를 JSON 형태로 클라이언트로 전송하고, 클라이언트에서는 해당 데이터를 JSON 파일로 저장하는 방식입니다. 브라우저에서는 JSON 파일을 지원하므로 이를 활용하여 파일 다운로드가 가능합니다.

0개의 댓글

관련 채용 정보