반응형 API
Vue는 반응형 API 시스템을 이용한다. 아래와 같은 기능들이 있는데, 자세한 내용은 따로 포스팅을 쓸 예정.
- reactive() : 반응형 객체를 만들 때 사용 - 객체만 가능
- ref() : 단일 값을 반응형으로 만들 때 사용 - 숫자, 문자열, 배열 등 모든 타입에서 사용 가능 / .value로 접근 및 변경
- computed() : 계산된 값을 자동으로 업데이트
- watch() : 특정 반응형 값의 변화를 감지하여 추가적인 작업을 수행할 때 사용
- watchEffect() : 종속성을 자동으로 추적하여 실행되는 반응형 감시자
- toRef(), toRefs() : reactive() 객체의 특정 속성을 ref()로 변환할 때 사용
- customRef() : 직접 반응형 상태의 동작을 제어할 때 사용
Vue.js의 반응형 API는 Vue 3에서 도입된 기능으로, 반응형 상태를 선언하고 관리할 수 있는 다양한 방법을 제공합니다. 기존 Vue 2에서는 data, computed, watch 등을 사용하여 반응형 데이터를 정의했지만, Vue 3에서는 Composition API를 활용하여 더 유연하고 모듈화된 방식으로 반응형 상태를 관리할 수 있습니다.
Vue.js
Vue.js - The Progressive JavaScript Framework
vuejs.org
파일 업로드 구현
Vue의 반응형 객체는 Proxy 방식으로 래핑되는데, 때문에 여러 컴포넌트가 파일 객체를 주고받아야 하는 경우 래핑된 객체를 안전하게 보내고 꺼내는 방식이 중요하다. 처음에는 단순히 파일 객체만 전달했는데 전달 과정에서 자꾸 누락이 생김... 실제로 구현하면서 변경된 코드들은 많지만, 간단히 fileData 구조만 비교하여 시행착오 과정을 포스팅한다.
일단 한줄로 요약하면, Vue의 반응형 시스템은 File 객체를 관리할 수 없다. File 객체를 그대로 data()나 ref()에 저장하면 직렬화할 수 없어서 오류가 발생하거나 데이터가 사라지는 문제. 또 로컬 스토리지나 DB에도 저장할 수 없는(API로 왔다갔다 불가능한) 형식이라는 거
첫 번째 시도 : 직접 File 객체 저장
const fileData = {
name: file.name,
size: file.size,
type: file.type,
_file: file
};
- Vue의 반응성 시스템이 File 객체를 직렬화하는 과정에서 객체의 내용이 손실
- Vue의 반응성 시스템은 JavaScript의 Proxy를 이용해 객체를 추적하는데, File 객체는 직렬화(Serialization)이 불가능한 내부 구조를 포함하고 있음
- 반응성 시스템이 File 객체를 Proxy로 래핑하려다 무시하는 경우 발생
- File 객체를 data()에 저장하거나, props로 전달하면 Vue가 JSON으로 변환하려 시도하면서 빈 객체 {} 또는 undefined로 처리되었음
- File 객체는 복잡한 구조를 가지고 있어, Vue의 reactive 시스템과 호환되지 않는 문제
import { markRaw, ref } from 'vue';
const file = ref(null);
const handleFileUpload = (event) => {
file.value = markRaw(event.target.files[0]); // Vue의 반응성 시스템에서 제외
};
- markRow()를 사용하여 반응성 시스템에서 제외할 수 있으나, File을 직접 저장하는 것이기 때문에 파일 미리보기 등의 기능을 구현하기 어려운 단점이 있음.
두 번째 시도 : Blob 사용
const blob = new Blob([file], { type: file.type });
const fileData = {
name: file.name,
size: file.size,
type: file.type,
_blob: blob
};
- File 객체와 같은 문제(직렬화 불가) 발생
- Vue가 내부적으로 Proxy로 감싸려다 직렬화 불가능한 데이터로 인식하여 무시해버림.
- 반응형 시스템에서 Blob의 내부 객체가 손실되는 문제
- Blob을 메모리에 저장할 수는 있지만, Vue의 ref()나 reactive()로 관리할 경우 내용이 손실될 수 있음.
- 결과적으로 빈 객체만 전달되어 파일을 재생성한 결과 일부 파일 데이터가 소실되어 15B 사이즈로 저장됨
const imageUrl = ref(null);
const handleFileUpload = (event) => {
const file = event.target.files[0];
imageUrl.value = URL.createObjectURL(file);
};
- URL.createObjectURL(blob) 사용 시 해결되나, Base64처럼 직렬화하여 JSON 형태로 저장할 수 없음. (Vuex 또는 API 전송 시 문제 발생) → 백엔드로 파일을 전송해서 백엔드에서 path, name 등 가공할 것이므로 pass.....
세 번째 시도 : ArrayBuffer 사용
const arrayBuffer = await file.arrayBuffer();
const fileData = {
name: file.name,
size: file.size,
type: file.type,
arrayBuffer: arrayBuffer
};
- Vue의 반응형 시스템에서 제대로 처리되지 않고, 복잡한 바이너리 데이터 구조가 모두 손실되는 문제
- ArrayBuffer는 바이너리 데이터를 저장하는 객체지만, Vue는 이를 자동으로 직렬화할 수 없음.
- Vue의 반응성 시스템이 ArrayBuffer를 감지하지 못하거나 JSON 변환 시 손실됨.
- API 요청 시 JSON.stringify(arrayBuffer)를 하면 [object ArrayBuffer]처럼 이상한 문자열이 전송됨
const reader = new FileReader();
reader.onload = () => {
const buffer = reader.result;
console.log(new Uint8Array(buffer)); // 바이너리 데이터를 Uint8Array로 변환
};
reader.readAsArrayBuffer(file);
- Uint8Array 또는 Blob으로 변환하여 사용 가능하지만 직접 다루기 어렵고 Vue의 반응형 시스템에서 자동으로 관리하기 어려운 단점이 존재하여 기각
네 번째 시도(성공) : Base64 사용
const base64 = await new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.readAsDataURL(file);
});
const fileData = markRaw({
name: file.name,
size: file.size,
type: file.type,
base64: base64
});
- Base64는 일반 문자열이므로 Vue의 반응성 시스템에서 안전하게 처리 가능 → 직렬화/역직렬화 안정적으로 동작
- 데이터 손실 없이 파일의 모든 정보를 보존할 수 있음
- + markRaw를 사용하여 불필요한 반응성 처리를 방지
Base64란?
- 바이너리 데이터를 문자열(String)로 변환하는 인코딩 방식.
- 모든 파일을 문자열 데이터로 변환할 수 있어 Vue의 반응성 시스템과 완벽하게 호환됨
- JSON 직렬화가 가능하므로, Vue의 ref(), reactive()를 통해 문제 없이 저장 및 관리할 수 있음
Base64만 성공한 이유
- 문자열이므로 Vue의 반응성 시스템에서 추적 가능 → ref()나 reactive()에 저장 시 Proxy로 감싸도 문제 없음
- JSON 직렬화 가능 → API 요청 시 Base64 문자열을 JSON으로 안전하게 전송 가능
- 브라우저에서 직접 미리보기 가능 → <img :src="base64Data" /> 형태로 바로 렌더링 가능
- 스토리지 저장 가능 → localStorage, sessionStorage, IndexedDB 등
Base64 단점
- 파일 크기 33% 증가 : 바이너리 데이터를 문자열로 변환하는 과정에서 용량 커짐 (이미지가 클 경우 성능 문제 발생하지만, 현재 개발 중인 플랫폼 특성상 10MB 정도의 파일로 제한하고 있기에 문제X)
- 압축 및 최적화가 어려움 : PNG, JPG 같은 파일은 원래 포맷에서 압축 가능하지만 비교적 압축 효율 떨어짐
- 메모리 사용량 증가 : ref()에 Base64 데이터 저장 시 브라우저 메모리 많이 차지함
요약
Vue 3에서 반응성 시스템(ref, reactive...)은 객체의 getter/setter를 감지하여 반응성을 유지하지만, Vue의 Proxy 반응성 시스템이 파일(File)과 블롭(Blob) 객체의 내부 데이터를 추적하지 않기 때문에 반응성을 잃어버릴 수 있음.
- Base64 방식을 사용하지 않고 파일 정보를 유지하는 방법
- ref() 대신 shallowRef() 사용 : 객체 내부 속성을 반응형으로 감지하지 않아 파일을 손상시키지 않음.
- FormData에 직접 추가 : ref()나 reactive()에 파일을 저장하지 않고, 이벤트 발생 시 FormData를 즉시 생성
- reactive()로 파일을 감싸는 경우 toRaw() 사용 : reactive() 내부 Proxy 제거
Vue에서 반응형으로 저장할 때 매우 적합하지만, 최종적으로 API에 전송할 때는 원본 파일을 사용하는 게 더 좋다고 함. 서버에 저장할 때는 Base64 대신 원본 파일을 FormData로 전송하는 방식으로 디벨롭 필요 ...
<script setup>
import { ref } from 'vue';
const base64Data = ref(null);
const fileData = ref(null);
const handleFileUpload = (event) => {
const file = event.target.files[0];
fileData.value = file; // 원본 파일 저장
const reader = new FileReader();
reader.onload = () => {
base64Data.value = reader.result; // Base64 인코딩
};
reader.readAsDataURL(file);
};
const uploadFile = async () => {
const formData = new FormData();
formData.append('file', fileData.value);
await fetch('/upload', {
method: 'POST',
body: formData,
});
alert('파일 업로드 완료!');
};
</script>
<template>
<div>
<input type="file" @change="handleFileUpload" />
<img v-if="base64Data" :src="base64Data" alt="Preview" />
<button @click="uploadFile">파일 업로드</button>
</div>
</template>
'⚙ Framework > Vue-뷰' 카테고리의 다른 글
[Vue] ESLint에서 TypeScript 코드 문법 인식되지 않는 경우 (0) | 2025.03.19 |
---|---|
[Vue.js] 설치 및 시작하기 (0) | 2025.01.21 |