앵귤러2 기반 웹앱에 파일 업로드 기능을 추가해야 할 일이 생겨서 검색을 했더니 다음의 두 라이브러리가 얻어 걸렸다.
- ng2-file-upload (https://github.com/valor-software/ng2-file-upload)
- ng2-uploader (https://github.com/jkuri/ng2-uploader)
둘 다 쉽게 파일 업로드를 붙일 수 있다. 두 라이브러리를 실험해보고 나서 ng2-file-upload를 선택했다. 그 이유는 구현하려는 기능에 조금 더 맞았기 때문이다.
ng2-file-upload의 기본 사용법
데모(http://valor-software.com/ng2-file-upload/) 사이트를 보면 ng2-file-upload의 기본 사용법을 볼 수 있는데, 진짜 쉽다. ng2-file-upload가 제공하는 FileUploader 클래스와 ng2FileDrop 디렉티브와 ng2FileSelect 디렉티브를 사용하면 된다.
나 같은 경우 앵귤러2 사이트의 튜토리얼 문서에 있는 과정을 따라서 프로젝트를 생성했다. ng2-file-upload를 사용하기 위해 package.json 파일에 ng2-file-upload에 대한 의존을 추가했다.
{
...
"dependencies": {
"@angular/common": "2.0.0-rc.5",
...
"bootstrap": "^3.3.6",
"ng2-file-upload": "1.0.3"
},
"devDependencies": {
"typescript": "^1.8.10",
"typings":"^1.0.4"
}
}
추가한 뒤에 npm install 명령어를 사용해서 ng2-file-upload를 다운로드 받았다.
그리고, 모듈 설정을 위해 systemjs를 사용했기 때문에 systemjs.config.js 파일에 ng2-file-upload에 대한 설정을 다음과 같이 추가해서 모듈 이름으로 접근할 수 있도록 했다.
(function (global) {
// map tells the System loader where to look for things
var map = {
'app': '/operation-app/app-testeditor', // 'dist',
'@angular': '/node_modules/@angular',
'rxjs': '/node_modules/rxjs',
'ng2-file-upload': '/node_modules/ng2-file-upload'
};
// packages tells the System loader how to load when no filename and/or no extension
var packages = {
'app': {main: 'test-editor-main.js', defaultExtension: 'js'},
'rxjs': {defaultExtension: 'js'},
'ng2-file-upload': {defaultExtension: 'js'}
};
...
...
System.config(config);
})(this);
파일 업로드 기능이 필요한 앵귤러 컴포넌트는 다음과 같이 ng2-file-upload 모듈을 임포트하면 된다.
import {FILE_UPLOAD_DIRECTIVES, FileUploader} from "ng2-file-upload/ng2-file-upload";
@Component({
selector: 'test-file-uploader',
templateUrl: './test-file-uploader.component.html',
directives: [FILE_UPLOAD_DIRECTIVES]
})
export class TestFileUploader {
public uploader:FileUploader = new FileUploader({url: '/my/upload/path');
...
}
FileUploader 클래스는 업로드할 파일 목록을 관리하고 파일을 전송하는 기능을 제공한다. ng2-file-upload 사이트의 데모 코드를 보면 다음과 같이 템플릿 코드에 디렉티브를 사용해서 업로드할 파일을 추가할 수 있다.
<div ng2FileDrop [uploader]="uploader" ....>
Base drop zone
</div>
<div ng2FileDrop [uploader]="uploader" ....>
Another drop zone
</div>
Multiple
<input type="file" ng2FileSelect [uploader]="uploader" multiple /><br/>
Single
<input type="file" ng2FileSelect [uploader]="uploader" />
ng2FileDrop 디렉티브로 지정한 영역에 파일을 드롭하거나 ng2FileSelect를 이용해서 파일을 선택하면, 해당 파일이 [uploader] 속성으로 지정한 FileUploader 객체에 추가된다.
이렇게 FileUploader에 추가한 파일을 업로드하려면 다음과 같이 FileUplaoder의 uploadAll()을 사용하면 된다. 또는 추가한 개별 파일별로 업로드를 할 수도 있다. 데모 사이트에서 완전한 코드를 확인할 수 있다.
<button type="button" class="btn btn-success btn-s"
(click)="uploader.uploadAll()" [disabled]="!uploader.getNotUploadedItems().length">
<span class="glyphicon glyphicon-upload"></span> Upload all
</button>
디렉티브 없이 FileUploader 직접 사용
ng2-file-upload가 그 자체로 좋지만 내가 필요한 건 ng2-file-upload가 제공하는 파일 업로드 기능이었고, 디렉티브를 통한 연동은 필요없었다. 그래서 디렉티브를 사용해서 파일 목록을 FileUploader에 추가하지 않고 직접 추가했다.
FileUploader 생성과 파일 추가
FileUploader 객체를 앵귤러 컴포넌트 생성자에서 직접 생성했다. <input type="file"> 버튼을 눌러 선택한 파일 목록을 받기 위한 메서드는 handleUploadFileChanged()이다.
@Component( ... )
export class TestFileUploader {
public uploader:FileUploader;
constructor() {
let uploadUrl = window.location.protocol + "//" + window.location.host + "/testfile/upload";
this.uploader = new FileUploader({url: uploadUrl});
...
}
handleUploadFileChanged(event) {
this.uploader.clearQueue();
let files:File[] = event.target.files;
let filteredFiles:File[] = [];
for (var f of files) {
if (f.name.endsWith(".pdf")) {
filteredFiles.push(f);
}
}
if (filteredFiles.length == 0) {
this.showGuide = true;
} else {
this.showGuide = false;
let options = null;
let filters = null;
this.uploader.addToQueue(filteredFiles, options, filters);
}
}
...
요구사항은 파일을 하나만 업로드하는 것이었기에 handleUploadFileChanged()는 먼저 현재 uploader.clearQueue())를 이용해서 uplaoder에 추가되어 있는 파일 목록을 지운다. 이를 제거하지 않으면 파일을 선택할 때마다 업로드할 파일이 추가되기 때문에, 실제 업로드를 수행할 때 마지막 선택한 파일을 포함한 이전에 선택한 모든 파일을 업로드한다.
위 코드에서는 확장자가 pdf인 파일을 걸러낸 뒤에 uploader.addToQueue()를 이용해서 업로드할 파일 목록을 직접 uploader에 추가했다.
handleUploadFileChanged()를 실행하기 위한 템플릿 코드는 다음과 같다.
<input type="file" (change)="handleUploadFileChanged($event)">
업로드와 완료 처리
업로드할 파일을 선택했다면 uploader.uploadAll()을 이용해서 선택한 파일을 업로드할 수 있다. 업로드를 완료한 뒤에 성공/실패 유무에 따라 알맞은 후속 작업(안내 문구 출력 등)을 하고 싶다면 uploader에 이벤트를 리스너를 등록하면 된다.
@Component( ... )
export class TestFileUploader {
public uploader:FileUploader;
private uploadResult:any = null;
constructor() {
let uploadUrl = window.location.protocol + "//" + window.location.host + "/testfile/upload";
this.uploader = new FileUploader({url: uploadUrl});
this.uploader.onSuccessItem = (item, response, status, headers) => {
this.uploadResult = {"success": true, "item": item, "response":
response, "status": status, "headers": headers};
};
this.uploader.onErrorItem = (item, response, status, headers) => {
this.uploadResult = {"success": false, "item": item,
"response": response, "status": status, "headers": headers};
};
this.uploader.onCompleteAll = () => {
this.handleUploadComplete();
};
}
uploadFile() {
this.uploader.uploadAll(); // 업로드 시작
}
private handleUploadComplete() {
console.log("upload compete : " + this.uploadResult.response);
if (this.uploadResult.success) {
...성공 메시지 출력
} else {
...실패 메시지 출력
}
}
FileUploader에는 onSuccessItem, onErrorItem, onCompleteAll 등의 이벤트 리스너를 등록할 수 있다. 이들 리스너를 등록하면 업로드 성공/실패 여부를 이벤트로 받아 그에 알맞은 처리를 할 수 있다. 위 코드는 한 개 파일만 업로드하므로 onSuccessItem 리스너와 onErrorItem 리스너에서 업로드 결과를 uploadResult 필드에 할당했다. 그리고, 모든 업로드 처리가 끝나면 불리는 onCompleteAll 리스너에서 업로드 성공/실패 결과에 따라 알맞은 처리를 수행하도록 했다.