티스토리 뷰

이미지 -> 이미지업로드 플러그인 -> 파일 로더 (FileRepository -> 업로드 어댑터 사용)

파일 로더
디스크에서 파일 읽어서 서버에 전송

업로드 어댑터
파일을 서버에 암호화해서 보내고 응답을 다시 파일 로더에 되돌려주는 역할

 

File repository 플러그인

CKEditor5에서 파일 업로드를 관리하는 핵심 플러그인이다. 

- FileRepository.createUploadAdapter() 팩토리 메서드를 사용하여 에디터 속에서 업로드 어댑터가 활성화 된다.

- 이미지 업로드와 같은 기능들은 유저가 업로드를 요청 할 때 마다 새로운 업로드 어댑터 인스턴스를 만들기 위해서 FileRepository의 API를 사용한다.

 

쉽게 말해서 그냥 업로드 어댑터를 만들어주는 역할이라고 생각하면 될듯.

FileRepository(파일 저장소, 저장된 파일을 서버에 올려야 하니 업로드 어댑터를 만들자) -> Upload Adapter(파일 서버에 전송하는놈)

 

이미지 업로드 플러그인이란?

유저와 상호작용하는 가장 상위의 플러그인이다.(예를들면, 유저가 파일을 에디터에 드래그앤 드랍 했을때 그 파일을 서버에 전송하고 컨텐츠에 이미지를 표시하는 역할을 함.)

이미지 업로드 플러그인은 업로드 어댑터를 생성하기 위해서 파일 레파지토리를 사용한다.

이미지 업로드 플러그인 -> File Repository -> upload adapter 와 같은 구조로 되어 있음.

업로드 어댑터의 upload()메소드가 실행되면 서버에 이미지를 업로드하고, 이 함수의 리턴값은 promise인데 이 promise의 리턴값을 활용하여 에디터에 이미지를 표시할 수 있다.


이미지 업로드 플러그인이 하는일
1. image의 palceholder
2. 에디터에 이미지 넣기
3. 프로그레스바 표시
4. 업로드가 끝나기전에 컨텐츠에서 제거되면 업로드 절차를 취소한다.

업로드 어댑터는 Promise를 사용하여 업로드 성공 여부를 에디터에게 노티한다.
또한, 업로드된 이미지의 URL을 이미지 업로드 플러그인에게 전달하고, 에디터 속의 image태그의 src와 srcset속성을 방금 그 URL로 설정한다.

실제로는 예외 처리 라던가, 에디터에 복사 붙여넣기를 처리하는 등 더 복잡하지만 이것을 모두 이미지 업로드 플러그인이 처리해주기 때문에 신경 쓰지 않아도 된다.

에디터에서 이미지 업로드를 위해서는 다음과 같은 2가지가 필요하다.
1. 이미지 업로드 플러그인(officail builds or 커스터마이징 CKEditor5라면 꼭 include하길)
2. 업로드 어댑터가 정의되어야 한다. 
- 이미 존재하는 업로드 어댑터를 사용하거나, 커스텀 업로드 어댑터를 사용 할 수 있다.

업로드 어댑터 자세히 살펴보기

커스텀 업로드 어댑터는 파일을 서버에 보내고 서버로부터의 응답을 에디터에 설정하는 역할을 한다.
이미지 업로드 어댑터든 파일 업로드 어댑터든 UploadAdapter interface를 구현하여 upload()와 abort()메소드를 구현해야 한다.

upload() 메소드는 promise를 리턴해야만 한다.
- promise는 업로드된 파일에 대한 정보를 담은 객체와 함께 resolved 된다.
- 에러에 의해서 rejected된 경우 에디터 컨텐츠에 아무것도 들어가지 않는다.

커스텀 업로드 어댑터의 가장 간단한 폼은 다음과 같다.

 

여기서 나오는

server.onUploadProgress()

server.upload()

server.abortUpload() 는 예시일뿐, 내가 직접 구현해야 하는 메소드 이다.

 

class MyUploadAdapter {
    constructor( loader ) {
        // 파일로더가 업로드 어댑터를 사용해서 이미지를 서버에 전송하기 때문에
        this.loader = loader; //파일로더
    }

    // 업로드를 시작한다.
    upload() {
        // 파일로더의 진행상황을 업데이트 하자.
        server.onUploadProgress( data => {
            loader.uploadTotal = data.total;
            loader.uploaded = data.uploaded;
        } );

        // 파일 업로드가 성공했을때 resolved될 promise를 리턴하자.(server.upload(file) 메서드에서 promise를 리턴 시키라는 뜻)
        return loader.file
            .then( file => server.upload( file ) );
    }

    // 업로드 프로세스 취소
    abort() {

       // upload() 메소드로부터 리턴된 프로미스를 reject해라.
        server.abortUpload();
    }


}

 

아까 위에서 FileRepository가 업로드 어댑터 인스턴스를 생성한다고 했다. 이때, 방금 내가 정의한 MyUploadAdapter를 생성하게끔 해주기 위해서 다음과 같이 선언해야 한다.

editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
    return new MyUploadAdapter( loader );

};

 

커스텀 업로드 어댑터 구현하기

class MyUploadAdapter {
    constructor( loader ) {  
        this.loader = loader;
    }     
	upload() {
        return this.loader.file
            .then( file => new Promise( ( resolve, reject ) => {
                this._initRequest();
                this._initListeners( resolve, reject, file );
                this._sendRequest( file );
            } ) );
    }
    abort() {
        if ( this.xhr ) {
            this.xhr.abort();
        }
    }
}
_initRequest() {
        const xhr = this.xhr = new XMLHttpRequest();
        //여기서는 POST 요청과 json으로 응답을 받지만 어떤 포맷으로 하든 너의 선택이다.
        xhr.open( 'POST', 'http://example.com/image/upload/path', true );
        xhr.responseType = 'json';
    }
//XHR 리스너 초기화 하기
    _initListeners( resolve, reject, file ) {
        const xhr = this.xhr;
        const loader = this.loader;
        const genericErrorText = `Couldn't upload file: ${ file.name }.`;
        xhr.addEventListener( 'error', () => reject( genericErrorText ) );
        xhr.addEventListener( 'abort', () => reject() );
        xhr.addEventListener( 'load', () => {
            const response = xhr.response;            
            // 이 예제에서는 XHR서버에서의 response 객체가 error와 함께 올 수 있다고 가정한다. 이 에러는
            // 메세지를 가지며 이 메세지는 업로드 프로미스의 매개변수로 넘어갈 수 있다.
           	
            if ( !response || response.error ) {
                return reject( response && response.error ? response.error.message : genericErrorText );
            }
            
            // 만약 업로드가 성공했다면, 업로드 프로미스를 적어도 default URL을 담은 객체와 함께 resolve하라. 
            // 이 URL은 서버에 업로드된 이미지를 가리키며, 컨텐츠에 이미지를 표시하기 위해 사용된다.
            resolve( {
                default: response.url
            } );
        } );
        
        // 파일로더는 uploadTotal과 upload properties라는 속성 두개를 갖는다.
        // 이 두개의 속성으로 에디터에서 업로드 진행상황을 표시 할 수 있다.
        if ( xhr.upload ) {
            xhr.upload.addEventListener( 'progress', evt => {
                if ( evt.lengthComputable ) {
                    loader.uploadTotal = evt.total;
                    loader.uploaded = evt.loaded;
                }
            } );
        }
    }
//데이터를 준비하고 서버에 전송한다.
    _sendRequest( file ) {
        // 폼 데이터 준비
        const data = new FormData();
        data.append( 'upload', file );

	// 여기가 인증이나 CSRF 방어와 같은 방어 로직을 작성하기 좋은 곳이다. 
        // 예를들어, XHR.setREquestHeader()를 사용해 요청 헤더에 CSRF 토큰을 넣을 수 있다.

        this.xhr.send( data );
    }

내가 만든 커스텀 업로드 어댑터 활성화 시키기

function MyCustomUploadAdapterPlugin( editor ) {
    editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
        // Configure the URL to the upload script in your back-end here!
        // 결국엔 내가 구현해 주어야 할 것은,
        // FileRepository가 어떤 업로드 어댑터를 사용하게 하느냐만 설정해주면 된다.
        // 나머지 이미지 업로드 플러그인, 파일 로더, FileRepository등등은 이미 만들어져 있다.
        return new MyUploadAdapter( loader );
    };
}

ClassicEditor
    .create( document.querySelector( '#editor' ), {
        extraPlugins: [ MyCustomUploadAdapterPlugin ],
    } )
    .catch( error => {
        console.log( error );
    } );

 

 

 

 

 

 

 

 

 

출처

https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/upload-adapter.html#how-does-the-image-upload-work

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함