왜 UploadCollection이 필요한가
SAP UI5 기반 비즈니스 애플리케이션에서 파일 업로드는 단순한 첨부 기능을 넘어, 송장 스캔본, 계약서 PDF, 제품 이미지, 품질 검사 사진 등 핵심 트랜잭션의 일부로 다뤄지는 경우가 많습니다. sap.m.upload.UploadCollection은 이러한 요구를 충족하기 위해 설계된 컨트롤로, 다중 파일 선택, 진행률 표시, 파일 메타데이터 관리, OData 백엔드 연동, CSRF 토큰 처리까지 한 컨트롤 안에서 다룰 수 있도록 구성되어 있습니다. 기존 sap.m.UploadCollection(Deprecated)을 대체하는 신규 컨트롤이며, UI5 1.62 이상에서 사용 가능합니다.
실무에서 파일 업로드 화면을 직접 처음부터 만들면 드래그앤드롭, 파일 크기 검증, MIME 타입 필터링, 업로드 중 취소, 멀티파트 인코딩, 진행률 콜백 등 챙겨야 할 항목이 매우 많아집니다. UploadCollection은 이 모든 기능을 추상화하여, 개발자는 비즈니스 로직(어디로 보낼 것인가, 어떻게 검증할 것인가)에만 집중하면 되도록 설계되어 있습니다.
핵심 구성 요소와 동작 흐름
UploadCollection은 내부적으로 세 가지 핵심 요소로 동작합니다. 첫째, FileUploader는 브라우저의 <input type="file">을 감싸는 저수준 컨트롤입니다. 둘째, UploadSet/UploadCollectionItem은 업로드된 파일 한 건의 메타데이터(파일명, 크기, 타입, 상태)를 표현합니다. 셋째, UploadCollectionParameter는 HTTP 헤더 또는 폼 파라미터를 동적으로 주입하는 메커니즘입니다. CSRF 토큰, Authorization 헤더, 사용자 정의 메타데이터를 백엔드로 전달할 때 반드시 거치는 통로입니다.
업로드 흐름을 단계별로 보면 다음과 같습니다. (1) 사용자가 파일을 선택하면 change 이벤트가 발생합니다. (2) 개발자는 이 시점에 헤더 파라미터(CSRF 토큰 등)를 주입하고 upload()를 호출합니다. (3) 업로드가 진행되는 동안 uploadProgress 이벤트가 반복적으로 발생하여 전송된 바이트 수를 알려줍니다. (4) 업로드가 끝나면 uploadComplete 이벤트가 발생하며, HTTP 상태 코드와 응답 본문을 확인할 수 있습니다. 이 흐름을 머릿속에 그려두면 디버깅이 한결 수월해집니다.
준비 사항과 환경 구성
아래 예제는 UI5 1.108 LTS 기준으로 작성되었으며, OData V2 게이트웨이 서비스(/sap/opu/odata/sap/ZMY_SRV)에 미디어 엔티티(Files)가 노출되어 있다고 가정합니다. 백엔드에서는 미디어 엔티티에 has_stream 속성을 부여하고, ABAP에서는 /IWBEP/IF_MGW_APPL_SRV_RUNTIME~create_stream을 구현하여 업로드된 바이너리를 받아 처리합니다. CSRF 토큰은 GET $metadata 호출 시 x-csrf-token: Fetch 헤더로 받아두고, POST 시 그대로 다시 보내는 방식을 일반적으로 사용합니다.
의존성 모듈은 sap/m/upload/UploadCollection, sap/m/upload/UploadCollectionParameter, sap/m/MessageToast 세 가지가 기본이며, XML 뷰에서는 네임스페이스 xmlns:upload="sap.m.upload"를 선언해야 합니다.
1단계: 기본 선언과 uploadUrl 설정
가장 먼저 XML 뷰에서 컨트롤을 선언하고, 업로드 대상 URL과 기본 제약 조건을 한 번에 지정합니다. 다중 파일 선택을 허용하려면 multiple="true"를 지정하며, 허용 확장자와 최대 파일 크기(MB 단위)를 함께 설정해 두면 사용자가 파일을 고르는 순간 1차 검증이 자동으로 이뤄집니다.
<mvc:View
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m"
xmlns:upload="sap.m.upload"
controllerName="zfiles.controller.Main">
<Page title="파일 업로드">
<upload:UploadCollection
id="uploadCol"
uploadUrl="/sap/opu/odata/sap/ZMY_SRV/Files"
multiple="true"
fileType="jpg,png,pdf"
maximumFilenameLength="55"
maximumFileSize="5"
change=".onFileChange"
uploadProgress=".onUploadProgress"
uploadComplete=".onUploadComplete"/>
</Page>
</mvc:View>
여기서 maximumFileSize="5"는 5MB를 의미하며, 이를 초과하는 파일을 사용자가 선택하면 fileSizeExceed 이벤트가 발생합니다. fileType은 확장자 기준 필터로, MIME 타입 필터링(mediaTypes 속성)과 병행하여 사용할 수 있습니다. 확장자만 검사하면 우회가 쉬우므로 서버 측 검증은 필수입니다.
2단계: CSRF 토큰 주입과 업로드 실행
OData V2 게이트웨이는 POST/PUT 요청 시 CSRF 토큰을 요구합니다. UI5의 ODataModel을 통해 토큰을 미리 가져온 다음, change 이벤트 시점에 UploadCollectionParameter를 통해 헤더로 주입하고 upload()를 호출해야 합니다. change 이벤트만으로는 실제 전송이 일어나지 않으며, 명시적으로 upload()를 호출해야 한다는 점이 핵심입니다.
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/m/upload/UploadCollectionParameter",
"sap/m/MessageToast",
"sap/m/MessageBox"
], function (Controller, UploadCollectionParameter, MessageToast, MessageBox) {
"use strict";
return Controller.extend("zfiles.controller.Main", {
onInit: function () {
// OData 모델에서 CSRF 토큰 선조회
var oModel = this.getOwnerComponent().getModel();
oModel.refreshSecurityToken();
},
_getCSRFToken: function () {
var oModel = this.getOwnerComponent().getModel();
return oModel.getSecurityToken();
},
onFileChange: function (oEvent) {
var oUpload = this.byId("uploadCol");
// 기존 헤더 초기화 후 재주입 (중복 방지)
oUpload.removeAllHeaderParameters();
oUpload.addHeaderParameter(new UploadCollectionParameter({
name: "x-csrf-token",
value: this._getCSRFToken()
}));
oUpload.addHeaderParameter(new UploadCollectionParameter({
name: "slug",
value: encodeURIComponent(oEvent.getParameter("files")[0].name)
}));
// 실제 전송 시작
oUpload.upload();
},
onUploadComplete: function (oEvent) {
var iStatus = oEvent.getParameter("status");
if (iStatus === 200 || iStatus === 201) {
MessageToast.show("업로드가 완료되었습니다.");
this.byId("uploadCol").getBinding("items").refresh();
} else if (iStatus === 403) {
// CSRF 토큰 만료 → 재발급 후 재시도 안내
this.getOwnerComponent().getModel().refreshSecurityToken();
MessageBox.warning("세션이 만료되어 토큰을 재발급했습니다. 다시 시도해주세요.");
} else {
MessageBox.error("업로드 실패 (HTTP " + iStatus + ")");
}
}
});
});
slug 헤더는 SAP Gateway에서 파일명을 ABAP 백엔드로 전달할 때 일반적으로 사용하는 관례입니다. 한글이 포함된 파일명은 encodeURIComponent로 인코딩하지 않으면 ABAP 측에서 깨지는 경우가 잦으므로 인코딩을 권장합니다.
3단계: 실시간 진행률과 프로덕션 고려사항
대용량 파일을 다룰 때는 사용자 피드백이 곧 신뢰성입니다. uploadProgress 이벤트로 전송 바이트 수를 받아 퍼센트로 환산하고, JSON 모델에 바인딩해 UI 컨트롤(예: ProgressIndicator)에서 즉시 반영하는 패턴이 일반적입니다. 추가로 동시 업로드 수 제한, 재시도 로직, 감사 로깅을 함께 구성하면 프로덕션 품질에 가까워집니다.
onUploadProgress: function (oEvent) {
var iLoaded = oEvent.getParameter("loaded");
var iTotal = oEvent.getParameter("total");
if (!iTotal) { return; } // 0 division 방지
var iPct = Math.round((iLoaded / iTotal) * 100);
var oUiModel = this.getView().getModel("ui");
oUiModel.setProperty("/uploadPct", iPct);
oUiModel.setProperty("/uploadLabel",
(iLoaded / 1024 / 1024).toFixed(2) + " MB / " +
(iTotal / 1024 / 1024).toFixed(2) + " MB");
// 감사 로깅 (10% 단위)
if (iPct % 10 === 0) {
jQuery.sap.log.info("[Upload] " + iPct + "% 진행", null, "zfiles.Upload");
}
},
onBeforeUploadStarts: function (oEvent) {
// 토큰 만료 직전 갱신 - 1시간 이상 머문 세션 대비
var oHeader = oEvent.getParameter("headerParameter");
if (oHeader.getName() === "x-csrf-token") {
oHeader.setValue(this._getCSRFToken());
}
},
onFileSizeExceed: function (oEvent) {
MessageBox.warning("파일 크기는 5MB를 초과할 수 없습니다.");
},
onTypeMissmatch: function (oEvent) {
MessageBox.warning("허용된 형식은 jpg, png, pdf 입니다.");
}
XML 측에는 진행률을 시각화할 컨트롤을 추가합니다.
<ProgressIndicator
percentValue="{ui>/uploadPct}"
displayValue="{ui>/uploadLabel}"
showValue="true"
state="Information"
visible="{= ${ui>/uploadPct} > 0 && ${ui>/uploadPct} < 100 }"/>
프로덕션 환경에서는 보안 관점도 챙겨야 합니다. 첫째, 클라이언트 측 확장자 검증만 신뢰하지 않고 백엔드에서 매직 넘버 기반 MIME 검증을 수행합니다. 둘째, 업로드된 파일은 가능하면 별도 스토리지(예: S3, Document Management Service)에 두고 애플리케이션 서버에는 메타데이터만 남깁니다. 셋째, 대용량 파일은 청크 업로드를 검토하되, UploadCollection 단독으로는 청크 분할을 지원하지 않으므로 필요 시 sap.ui.unified.FileUploader를 직접 사용하거나 별도 라이브러리를 조합해야 합니다.
흔히 마주치는 문제와 해결 접근
Q1. 업로드 시 HTTP 403(Forbidden)이 떨어진다. 대부분 CSRF 토큰 누락 또는 만료가 원인입니다. oModel.refreshSecurityToken()으로 토큰을 다시 받고, 헤더 이름이 정확히 x-csrf-token(소문자, 하이픈)인지 확인합니다. SAP Gateway 환경에 따라 토큰 유효 기간이 짧을 수 있으므로 uploadComplete에서 403을 만나면 한 번 재시도하는 로직을 두면 안정적입니다.
Q2. 한글 파일명이 백엔드에서 깨진다. 브라우저가 multipart 폼 데이터에 파일명을 그대로 실어 보내지만, ABAP 측 인코딩 변환이 매끄럽지 않은 경우가 많습니다. slug 헤더에 encodeURIComponent로 인코딩한 이름을 따로 보내고, ABAP에서 cl_http_utility=>unescape_url로 복원하는 방식이 권장됩니다.
Q3. upload()를 호출했는데 네트워크 요청이 가지 않는다. change 이벤트 안에서 비동기 작업(예: 토큰 fetch)을 기다리지 않고 곧바로 upload()를 호출했을 가능성이 큽니다. 토큰을 먼저 onInit에서 미리 받아 캐시하거나, Promise로 토큰 수신을 보장한 뒤 upload()를 호출하도록 흐름을 정리합니다. 또한 uploadUrl 속성이 비어 있으면 조용히 실패하므로 콘솔에서 컨트롤 상태를 먼저 점검합니다.
Q4. 진행률이 100%까지 가지 않고 99%에서 멈춘다. 브라우저 XHR의 progress 이벤트는 전송 완료 직전 마지막 청크에서 한 번 더 호출되지 않을 수 있습니다. UI에서는 uploadComplete가 호출되면 강제로 100%로 보정한 뒤 잠시 후 인디케이터를 숨기는 처리를 권장합니다.
확장 주제와 다음 단계
UploadCollection을 기본기로 익혔다면 다음 주제로 넘어가면 활용 범위가 크게 넓어집니다. 첫째, OData V4 + CAP 환경에서 미디어 엔티티를 다루는 방법(@Core.MediaType 어노테이션, readStream/createStream 핸들러)을 익혀두면 신규 프로젝트에서 바로 쓸 수 있습니다. 둘째, SAP Document Management Service와 연동하면 파일을 CMIS 기반 리포지토리에 저장하고 버전 관리, 권한 제어를 위임할 수 있습니다. 셋째, Fiori Elements의 FileUpload 어노테이션을 쓰면 List Report/Object Page에 별도 코딩 없이 업로드 UI를 노출할 수 있습니다. 넷째, 드래그앤드롭 영역(sap.ui.core.dnd.DropInfo)과 결합하면 사용자 경험을 한층 끌어올릴 수 있습니다.
댓글 0
아직 댓글이 없습니다.