[ COMMA 아카이브 ] #1. 나의 Dev-Log : 다중 이미지 업로드 구현

BOOa·2025년 3월 26일

Archive

목록 보기
9/17
post-thumbnail

구성 흐름 : 파일 선택 → 미리보기 생성 → 상위 컴포넌트로 데이터 전달 → 서버 업로드 준비 → 업로드

🔎 컴포넌트 구조

📍 PostEditImg

: 다중 이미지 업로드 UI와 로직을 담당하는 부모 컴포넌트
  • 최대 4개의 이미지 슬롯
  • 이미지 미리보기와 업로드 트리거 관리

📍 PostEditImgCard

: 개별 이미지 카드를 렌더링하는 자식 컴포넌트
  • 이미지가 있으면 미리보기를 표시하고, 없으면 업로드 버튼(+)을 표시
  • 크기와 투명도를 props를 받아 동적으로 스타일링

🎨 이미지 업로드 UI

  • <input type="file" multiple>를 4개 생성하여 각 슬롯에 연결
  • v-for를 사용해 인덱스 별로 파일 입력을 동적으로 생성
  • hidden 클래스로 input을 숨기고, PostEditImgCard 클릭 시 triggerFileSelect로 파일 선택을 트리거
  • 이미지 업로드 후 미리보기는 URL.createObjectURl(file) 로 생성

🔗 임시 URL 생성

  • URL.createObjectURL(file) 은 파일 객체를 가리키는 임시 URL을 생성해준다. 이 URLdmf <img> 태그의 src 속성에 설정하면 사용자가 업로드한 이미지를 화면에 미리보기로 보여줄 수 있다.

✔ㅤ메모리 관리

  • createObjectURL 로 생성된 URL은 브라우저가 자동으로 해제하지 않기 때문에, 불필요한 메모리 사용을 방지하려면 사용이 끝난 후 URL.revokeObjectURL(url)을 호출하여 해제해야 한다.

[ 기존 코드 ]

handleFileChange(event, index) {
  const files = event.target.files;
  if (!files || files.length === 0) return;

  const newImages = Array.from(files).map((file) => ({
    file,
    preview: URL.createObjectURL(file),
  }));

  this.$emit("addImage", { index, images: newImages });
  event.target.value = "";
},

[ 수정 코드 ]

handleFileChange(event, index) {
  const files = event.target.files;
  if (!files || files.length === 0) return;
  
  if (this.images[index] && this.images[index].preview) {
    URL.revokeObjectURL(this.images[index].preview);
  }

  const newImages = Array.from(files).map((file) => ({
    file,
    preview: URL.createObjectURL(file),
  }));

  this.$emit("addImage", { index, images: newImages });
  event.target.value = "";
},

💬 코멘트

미리보기 구현에만 집중하다 보니, 이후의 처리를 충분히 고려하지 못했던 것 같다. 그래서 URL.createObjectURL 을 적용했지만, 메모리 누수를 방지하기 위해 URL.revokeObjectURL을 사용해야 한다는 점을 간과했다. 앞으로는 이러한 후처리까지 신중하게 고려하며 개발해야겠다.



🚨 이미지 추가 시 슬롯 위치 벗어남

🚨 문제점

이미지를 추가하면 의도된 슬롯에 위치해야 하지만, 제대로 렌더링 되지 않고, 레이아웃을 벗어나 예상치 못한 위치에 렌더링 되는 문제 발생

<post-edit-img-card
  v-if="images.length > 0"
  :imgSrc="images[0].preview || images[0]"
  :size="'w-[400px] h-[400px] lg:w-[440px] lg:h-[440px]'"
  :opacity="100"
  @click="handleRemoveImage(0)"
></post-edit-img-card>

📎 Vue의 동작 방식

  • vue는 화면을 그리기 위해 Virtual DOM 이라는 가상 화면을 사용한다. 이 가상 화면은 실제 HTML을 미리 계산해서 효율적으로 업데이트 한다.
  • v-if만 있을 때는, 조건이 거짓일 때 그 부분이 없어지는데, vue는 혼란스러울 수 있다.
  • postEditImgCard 는 같은 컴포넌트를 재사용하기 때문에 이 부분에서 혼란이 생길 수 있다.

💡 문제 분석

  • images.length > 0 일 때 0번 슬롯에 미리보기를 보여주고 클릭 시 해당 이미지를 삭제하게 조건부 처리를 해둔 상태
  • 하지만 images.length > 0이 거짓일 때 어떻게 할지 명확하게 지정되어 있지 않고 있다.
  • 결과적으로 이미지를 추가하면, 0번 슬롯에 미리보기가 들어가야 하지만 Vue가 레이아웃을 잘못 재정렬해서 다른 곳에 배치되게 만들었다는 가정
<post-edit-img-card
  v-else
  @click="triggerFileSelect(0)"
></post-edit-img-card>

✅ v-else 를 추가

  • v-else를 추가하여 images.length > 0이 거짓일 때 렌더링 할 조건을 명확하게 함
  • v-else가 없었을 때는 갱신 과정이 불안정해서 0번 슬롯이 레이아웃을 벗어났지만, 규칙이 제대로 잡히니 깔끔하게 갱신됨

🧐 다른 해결법 : key 속성으로 위치 고정

  • key 속성을 주어서, Vue가 같은 컴포넌트를 새로 그리더라도 각각을 구분할 수 있게 함
<post-edit-img-card
  v-if="images.length > 0"
  :key="'image-' + images[0].preview"
  :imgSrc="images[0].preview"
</post-edit-img-card>

📍 구현 로직 요약

<!-- 이미지 영역 -->
<post-edit-img-card
  v-if="images.length > 0"
  :imgSrc="images[0].preview || images[0]"
  :size="'w-[400px] h-[400px] lg:w-[440px] lg:h-[440px]'"
  :opacity="100"
  @click="handleRemoveImage(0)"
></post-edit-img-card>
  • 게시글 업로드와 수정 기능에 컴포넌트를 같이 사용하므로, 업로드된 첫 번째 이미지가 있으면 렌더링하여 표시하도록 했다.
  • 또한 @click 이벤트를 추가하여 클릭 시 해당 이미지를 삭제할 수 있게 했다.
<post-edit-img-card
  v-else
  @click="triggerFileSelect(0)"
></post-edit-img-card>
  • 만약 첫 번째 이미지가 없다면, 클릭 시 파일 업로드가 되도록 했다.

🛠️ 추가 개선점

  • 드래그 앤 드랍 기능 추가 : 이미지 업로드 UX 개선
  • 이미지 추가/삭제 시 애니메이션 추가 : 부드러운 전환
  • 파일 크기 유효성 검사 : 용량이 클 경우에 대한 검증 부재

0개의 댓글