처음으로 Vue 기술을 적용하여 간단한 웹 프로그램을 개발하는 과정에서 경험한 유용한 것들을 순서로 기술하였다. ( WebPack 를 사용하여 간단한 웹 페이지를 만들어본 경험이 많은 도움이 되었다. )
UI Kit
화면 UI 구현은 Vue-Material-Kit 을 사용하였다. 백문이 불여일견(百聞不如一見) 이라고 했던가 Vue 를 잘 모르는 상황에서 개발을 시작하는데 많은 도움이 되었다.
인증
JWT 기반의 인증을 사용하는 것이 보편적인 것 같으며 관련 코드는 JWT authentication from scratch with Vue.js and Node.js 자료를 참고하였다. 참고로 REST 통신 부분은 원 소스 방식이 아닌 axios 를 사용하였다.
Service 객체는 통신만 담당하고 결과 데이터는 Store 에 저장된다. UI는 이벤트를 통하여 결과를 전달 받는 소스 구현 방식이 생소하게 보인다. (아주 오래전 경험한 어도비 FLEX 의 MVC 프레임워크인 Cairngorm 기반 응용 프로그램 개발이 연상되기도 한다.)
Dynamic Route Matching
동적 라우팅 매칭을 이용하여 Spring 의 @PathVariable 와 같이 구현하였다. 정규식을 지원하기 때문에 파라메터 값을 String 이 아닌 Number 값으로 전달할 수 도 있다. 아쉬운 것은 route 가 아닌 history back 경우는 Number 타입이 아닌 String 타입으로 값이 전달되어 경고가 발생하는 이슈가 있었다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
path: "/albums", | |
name: "albums", | |
components: { default: Albums, header: MainNavbar, footer: MainFooter }, | |
props: { | |
header: { colorOnScroll: 400 }, | |
footer: { backgroundColor: "black" } | |
}, | |
beforeEnter: ifAuthenticated | |
}, | |
{ | |
path: "/albums/:albumId", | |
name: "album", | |
components: { default: Album, header: MainNavbar, footer: MainFooter }, | |
props: { | |
header: { colorOnScroll: 320 }, | |
default: true, | |
footer: { backgroundColor: "black" } | |
}, | |
beforeEnter: ifAuthenticated | |
}, |
Editor
간단하게 CKEditor5 를 이용하여 구현이 가능했지만 첨부 이미지 처리를 위하여 사용자 정의 이미지 어뎁터를 구현해주어야 하는 이슈가 있었으나 홈페이지에서 관련 예제가 제공되어 기술적 어려움은 없었다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Vue from "vue"; | |
import { authHeader } from "@/utils"; | |
export default class ImageUploadAdapter { | |
constructor( loader , url ) { | |
// The file loader instance to use during the upload. | |
this.loader = loader; | |
this.url = url || `${Vue.Constants.API_ROOT_URL}/data/images/0/upload`; | |
} | |
// Starts the upload process. | |
upload() { | |
return this.loader.file | |
.then( file => new Promise( ( resolve, reject ) => { | |
this._initRequest(); | |
this._initListeners( resolve, reject, file ); | |
this._sendRequest( file ); | |
} ) ); | |
} | |
// Aborts the upload process. | |
abort() { | |
if ( this.xhr ) { | |
this.xhr.abort(); | |
} | |
} | |
// Initializes the XMLHttpRequest object using the URL passed to the constructor. | |
_initRequest() { | |
const xhr = this.xhr = new XMLHttpRequest(); | |
// Note that your request may look different. It is up to you and your editor | |
// integration to choose the right communication channel. This example uses | |
// a POST request with JSON as a data structure but your configuration | |
// could be different. | |
xhr.open( 'POST', this.url , true ); | |
xhr.responseType = 'json'; | |
} | |
// Initializes XMLHttpRequest listeners. | |
_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; | |
// This example assumes the XHR server's "response" object will come with | |
// an "error" which has its own "message" that can be passed to reject() | |
// in the upload promise. | |
// | |
// Your integration may handle upload errors in a different way so make sure | |
// it is done properly. The reject() function must be called when the upload fails. | |
if ( !response || response.error ) { | |
return reject( response && response.error ? response.error.message : genericErrorText ); | |
} | |
// If the upload is successful, resolve the upload promise with an object containing | |
// at least the "default" URL, pointing to the image on the server. | |
// This URL will be used to display the image in the content. Learn more in the | |
// UploadAdapter#upload documentation. | |
resolve( { | |
default: getDownloadUrl(response[0]), | |
} ); | |
} ); | |
// Upload progress when it is supported. The file loader has the #uploadTotal and #uploaded | |
// properties which are used e.g. to display the upload progress bar in the editor | |
// user interface. | |
if ( xhr.upload ) { | |
xhr.upload.addEventListener( 'progress', evt => { | |
if ( evt.lengthComputable ) { | |
loader.uploadTotal = evt.total; | |
loader.uploaded = evt.loaded; | |
} | |
} ); | |
} | |
} | |
// Prepares the data and sends the request. | |
_sendRequest( file ) { | |
// Prepare the form data. | |
const data = new FormData(); | |
data.append( 'upload', file ); | |
// Important note: This is the right place to implement security mechanisms | |
// like authentication and CSRF protection. For instance, you can use | |
// XMLHttpRequest.setRequestHeader() to set the request headers containing | |
// the CSRF token generated earlier by your application. | |
this.xhr.setRequestHeader("Authorization", authHeader().Authorization); | |
// Send the request. | |
this.xhr.send( data ); | |
} | |
} | |
function getDownloadUrl(item) { | |
return encodeURI(`${Vue.Constants.API_ROOT_URL}/download/images/${item.linkId}`); | |
} | |
export function ImageUploadAdapterPlugin( editor ) { | |
editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => { | |
// Configure the URL to the upload script in your back-end here! | |
return new ImageUploadAdapter( loader ); | |
}; | |
} |
댓글 없음:
댓글 쓰기