[Vue] 파일 업로드 구현 - 반응형 API 시스템에서 File 객체 불러오는 방법
반응형 APIVue는 반응형 API 시스템을 이용한다. 아래와 같은 기능들이 있는데, 자세한 내용은 따로 포스팅을 쓸 예정.reactive() : 반응형 객체를 만들 때 사용 - 객체만 가능ref() : 단일 값을 반응형으
idox.tistory.com
이전 포스팅에 업로드한 바와 같이, Vue에서는 반응성 객체 내에서 file_, blob_ 정보를 유지하기 위해 Base64 인코딩 방식을 사용해야 한다. 하지만 서버에 Base64 형태로 데이터를 전송하게 되면 용량이 33% 증가한다는 점을 비롯한 단점이 존재하여 MultiPart 형태의 File 객체로 다시 변환하여 전송하는 과정이 필요하다.
BLOB이란?
BLOB(Binary Large Object)는 이미지, 비디오, 오디오, PDF 등과 같은 대용량의 바이너리 데이터를 저장하는 데이터 유형이다. DB에서는 텍스트가 아닌 바이너리 데이터(0, 1)를 저장하는 데 사용되며, 일반적으로 파일 저장소 역할을 한다.
- 텍스트가 아닌 이진 데이터(이미지, 동영상, 문서 등) 저장 가능
- Database 내에서 직접 저장하거나 파일 경로만 저장할 수도 있음
- 파일 경로(VARCHAR)와 비교해 DB 내에서 파일을 직접 관리 가능
- 유형 : TINYBLOB (최대 255B), BLOB (최대 64KB), MEDIUMBLOB (최대 16MB), LONGBLOB(최대 4GB)
파일 업로드 구현
Vue 3 + JavaScript, Spring Boot + MyBatis + Oracle 환경에서 구현한 코드와 로직은 다음과 같다.
- [F] 파일 선택 시 handleFiles() 이벤트 핸들러에서 Base64 변환 후 markRaw()를 사용해 반응성 제거
- 업로드 및 변경된 파일은 props에 목록 추가 후 emit()으로 업데이트
- [F] Base64 데이터를 포함한 전체 파일 리스트를 FormData에 담아 백엔드로 전송하기 위한 Api 호출
- FormData.append 이후 axios를 사용해 multipart/form-data로 서버 전송
- [B] 파일을 MultipartFile로 받음(@RequestParam("file") MultipartFile file)
- 파일 저장 Request DTO를 생성한 뒤 서비스 계층에 전달
- [B] 파일을 저장하고 DB에 파일 정보(Metadata) 등록
- file.getBytes()로 바이너리 데이터 추출 후 BLOB 컬럼에 저장하거나, 물리적 파일 시스템에 저장 후 경로만 DB에 저장
1. 프론트엔드 (Vue)
파일이 업로드되는 .vue 파일에서 파일 업로드 시 동작할 이벤트 핸들러를 생성하고, 다음과 같이 처리한다. 파일이 추가되면 데이터를 Base64 방식으로 읽고, markRaw()를 사용해 File 객체가 Proxy로 감싸지지 않도록 처리한다.
const handleFiles = async (files) => {
try {
// ... 기타 코드들
const newFiles = [...props.modelValue];
for (const file of Array.from(files)) {
// ... 기타 검증 코드들
// 파일 데이터를 Base64로 읽기
const base64 = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(e);
reader.readAsDataURL(file);
});
// markRaw를 사용하여 Vue의 반응성 시스템에서 제외
const fileData = markRaw({
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
base64: base64, // Base64 문자열로 저장
});
newFiles.push(fileData);
}
emit("update:modelValue", newFiles);
} catch (error) {
// ... 예외 처리 로직
}
};
해당 파일을 백엔드에 Multipart 형태로 전송하기 위해 Vue의 composable 방식으로 파일 처리 함수를 생성한다.
Base64로 저장된 파일을 File 객체로 변환하여 파일 업로드가 가능하도록 처리하고, 해당 파일을 FormData에 담아 서버에 보낼 수 있도록 변환한다.
- 파일 목록에서 Base64 데이터가 존재하는 파일만 필터링 (혹시 파일 정보가 소실된 경우를 방지)
- fileData.base64.split(",")[1] : Base64 문자열에서 헤더 제거 (ex. data:image/png;base64)
- window.atob(base64Data) : Base64 문자열을 바이너리 데이터로 디코딩
- Base64 형태는 Vue 반응성 시스템 내에서 파일 정보를 유지하기 위한 것으로, 서버에 전송할 때는 다시 디코딩
- Uint8Array : 바이너리 데이터를 Blob으로 변환
- new File : Blob 데이터를 새로운 File 객체로 변환
- 변환된 File 객체를 반환하여 업로드에 사용 (Api 호출)
export function useFileProcessing() {
// Base64 파일 처리 함수
const processBase64Files = (fileList) => {
if (!fileList || fileList.length === 0) return [];
return fileList
.filter((fileData) => fileData.base64) // Base64 데이터가 있는 파일만 필터링
.map((fileData) => {
try {
// Base64 데이터에서 'data:image/png;base64,' 부분 제거
const base64Data = fileData.base64.split(",")[1];
// Base64 문자열을 바이너리로 변환
const binaryString = window.atob(base64Data);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
// Blob으로 변환
const blob = new Blob([bytes], { type: fileData.type });
// Blob을 File 객체로 변환
return new File([blob], fileData.name, {
type: fileData.type,
lastModified: fileData.lastModified,
});
} catch (error) {
console.error("[파일 처리 오류]:", error);
return null;
}
})
.filter((file) => file !== null); // 오류가 난 파일은 제외
};
return {
processBase64Files,
};
}
Axios를 이용하여 FormData를 서버에 전송할 때는 multipart/form-data 형식으로 보내야 하므로, 헤더에 다음 정보를 추가한다.
headers: {
"Content-Type": "multipart/form-data",
},
2. 백엔드 (Spring)
Controller
- 클라이언트에서 multipart/form-data로 받은 파일을 처리하기 위해 파일을 MultipartFile 객체로 받는다.
- 파일 정보를 Request DTO로 변환하여 Service 계층에 전달한다.
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ResultResponse<FileUploadResDTO>> uploadFile(
@RequestParam("file") MultipartFile file,
// ... 기타 필요한 @Param
) {
// 파일 정보를 Request DTO로 변환
FileCommandReqDTO fileCommandReqDTO = FileCommandReqDTO.builder()
.file(file)
.build();
// 파일 업로드 로직
FileUploadResDTO response = fileCommandInPort.uploadFile(fileCommandReqDTO);
return ResponseEntity.ok(ResultResponse.success(HttpStatus.OK.value(), response));
}
Service
- 물리 파일을 서버에 저장하고, DB에 파일 메타데이터를 저장한다.
@Override
public FileUploadResDTO uploadFile(FileCommandReqDTO fileCommandReqDTO) {
MultipartFile file = fileCommandReqDTO.getFile(); // 업로드된 파일 가져오기
// ... 파일 유효성 검사 로직
try {
String originalFilename = file.getOriginalFilename();
String path = fileCommandOutPort.createUploadPath(originalFilename);
String name = StringUtils.getFilename(path);
// 파일 저장 IO 호출 (Repository)
fileCommandOutPort.savePhysicalFile(path, file); // 물리
ReportFile savedFile = fileCommandOutPort.insertFile(reportFile); // 메타(DB)
// ... Response DTO 반환
} catch (Exception e) {
log.error("파일 업로드 실패", e);
}
}
Repository
- DB에 파일 정보(파일 이름, 경로, 파일 크기 등의 메타데이터)를 저장한다.
- 파일을 물리적인 파일 시스템에 저장한다.
/**
* 파일 정보 DB에 저장
*
* @param reportFile 저장할 파일 정보 객체
* @return 저장된 파일 정보 객체
*/
@Override
public ReportFile insertFile(ReportFile reportFile) {
log.info("save file to DB");
fileCommandMapper.insertFile(reportFile);
return reportFile;
}
/**
* 업로드된 파일을 실제 파일 시스템에 저장하는 메서드
*
* @param physicalPath 파일이 저장될 경로
* @param file 업로드된 파일 객체
*/
@Override
public void savePhysicalFile(String physicalPath, MultipartFile file) {
log.debug("save physical file");
try {
// 1. 저장할 파일의 전체 경로 설정
Path targetPath = Paths.get(properties.getUploadDir(), physicalPath);
// 2. 파일이 저장될 디렉토리가 없으면 생성
Files.createDirectories(targetPath.getParent());
// 3. 업로드된 파일을 저장 (기존 파일이 있으면 덮어쓰기)
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new FileException(FileErrorCode.FILE_UPLOAD_FAILED);
}
}
Oracle (MyBatis)
- DB에 메타 데이터를 저장하는 SQL 쿼리를 수행한다.
<!-- 파일 업로드 정보 저장 -->
<insert id="insertFile">
<selectKey keyProperty="id" resultType="java.lang.Long" order="BEFORE">
SELECT REPORT_FILE_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO REPORT_FILE (
ID,
REPORT_ID,
PATH,
NAME,
ORIGIN_NAME,
CREATED_BY,
CREATED_AT,
UPDATED_BY,
UPDATED_AT
) VALUES (
#{id, jdbcType=NUMERIC},
#{reportId, jdbcType=NUMERIC},
#{path, jdbcType=VARCHAR},
#{name, jdbcType=VARCHAR},
#{originName, jdbcType=VARCHAR},
#{createdBy, jdbcType=VARCHAR},
SYSDATE,
#{createdBy, jdbcType=VARCHAR},
SYSDATE
)
</insert>
여기까지 구현 시 지정한 업로드 디렉토리 내에 물리적으로 파일이 저장되고, 파일 정보는 DB에 저장된다. Blob 객체의 경우 DB에 직접 저장할 수도 있으나 유지보수성 향상을 위해서는 파일을 물리적으로 저장하는 게 좋다고 한다.
파일 다운로드 프로세스
본문의 방식으로 업로드 된 파일을 클라이언트가 다시 다운로드할 수 있도록 하려면, 백엔드에서 저장된 파일을 제공하고 프론트엔드에서 이를 받아 다운로드하도록 처리해야 한다.
개요
- GET요청 : 클라이언트의 다운로드 요청
- 사용자가 다운로드 버튼을 클릭하면, 파일의 ID 또는 파일명을 서버로 요청한다.
- 백엔드에서 파일 찾기
- 요청된 ID 또는 파일명을 기준으로 데이터베이스에서 파일 정보를 조회
- 파일이 물리적으로 저장된 경로를 찾거나 BLOB 데이터 조회 (위 경우 파일 경로 조회)
- 백엔드에서 파일 응답
- 파일이 존재하면 Content-Disposition: attachment 헤더를 설정하여 바이너리 데이터를 클라이언트에 전송
- 프론트엔드에서 파일 다운로드 처리
- Axios 또는 Fetch API를 사용하여 파일 요청 (위 경우 Axios)
- Blob 데이터를 생성하여 사용자 브라우저에서 다운로드 트리거
상세 프로세스
- 프론트엔드에서 파일 다운로드 요청
- 사용자가 다운로드 버튼 클릭 시 Axios를 통해 백엔드에 GET 요청 전송
- 파일을 Blob 형식으로 받아 JavaScript에서 다운로드 링크 생성
- 백엔드에서 파일 제공
- 백엔드에서 파일명 또는 ID를 기반으로 파일 조회
- 파일 경로에서 파일을 직접 읽어 ResponseEntity로 클라이언트에 전송
- 프론트엔드에서 Blob 데이터 처리
- 백엔드에서 응답한 파일 데이터를 Blob으로 변환
- URL.createObjectURL(blob)을 사용해 다운로드 링크를 생성하고, a 태그를 클릭 이벤트로 실행
'⚙ Framework' 카테고리의 다른 글
[Vue+Spring] 파일 검증 로직 구현 (+ MIME 타입, 시그니처란?) (0) | 2025.02.25 |
---|