[JavaScript] SpringBoot : 다중 이미지 미리보기(<input type = "file">, 이미지 리스트 동적 생성, 이미지 추가 및 삭제)
목적
- 파일 여러개 미리보기 기능 구현
- 미리보기 이미지 클릭시 이미지 삭제
- 미리보기 이미지들을 서버에 전송할 수 있도록 배열에 저장
이번 프로젝트에서 메인 기능 중 하나인 갤러리는 여러 이미지를 서버에 보내고, 서버로부터 여러 이미지를 받아와서 화면에 뿌려줘야 한다.
가장 처음 단계는 <input type = "file"> 태그를 통해 이미지 파일을 받아서 화면에 미리보기 표시를 해주는 것이다.
JS로 어떻게 처리했는지 정리해보도록 하겠다.
결과화면은 아래와 같다.
[JSP]
<li class = "hide">
<label for ="upload">파일선택</label>
<input type = "file" name = "upload" id = "upload" accept = "image/gif, image/png, image/jpeg" multiple="multiple" style = "display:none;">
</li>
<div id = "imagePreviews">
<ul id = "forUpload-ul" style = "display : none;"></ul>
</div>
우선 JSP 쪽의 태그는 위와 같다.
이전에 프로필 사진 기능 구현 내용을 정리하면서 이미지를 받는 <input> 태그의 name을 잘 설정해줘야 한다고 했다.
이유는 Spring에서 해당 name을 가지고, 전달받은 모델의 setter를 찾아 매핑하기 때문이다.
만약 Ajax로 FormData를 생성해서 넘겨준다면, 생성한 FormData에 들어갈 name을 전달하는 모델의 setter와 적절하게 일치시켜주면 된다(관련 내용은 여기).
아무튼 <input type = "file" multiple = "multiple"> 을 통해 이미지를 한 장 혹은 그 이상 입력받도록 해뒀다.
아래 태그들은 업로드된 이미지를 띄워줄 부분이다.
새로 이미지가 들어오면 그 이미지를 읽어서 아래 <li>태그를 동적으로 추가하여 표시하도록 하기위해 미리 틀을 만들어뒀다.
[JavaScript]
let curImageCount = 0;
let imgSubmitOn = false;
let fileArray = [];
$(function(){
const upload = document.getElementById("upload");
const imagePreviews = document.getElementById("imagePreviews");
const uploadUL = document.getElementById("forUpload-ul");
let fileArrayIndex = 0;
let maxImageCount = 4;
upload.addEventListener("change", function(){
let files = this.files;
curImageCount += files.length;
if(curImageCount > maxImageCount){
curImageCount = uploadUL.childElementCount;
alert(maxImageCount + '개까지만 등록 가능합니다.');
return;
}
if(uploadUL.childElementCount > maxImageCount-1){
alert(maxImageCount + '개까지만 등록 가능합니다.');
return;
}
for(let i = 0; i < files.length; i++){
let file = files[i];
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(){
uploadUL.style.display = "";
let imageSrc = reader.result;
let imgLi = document.createElement("li");
uploadUL.appendChild(imgLi);
let selectedImg = document.createElement("img");
selectedImg.src = imageSrc;
selectedImg.className = "preview-img";
imgLi.appendChild(selectedImg);
let delBtn = document.createElement("input");
delBtn.type = "button";
delBtn.className = "preview-del-btn";
delBtn.value = "X";
delBtn.dataset.fileIndex = fileArrayIndex;
fileArrayIndex++;
imgLi.appendChild(delBtn);
fileArray.push(file);
}
}
checkImg();
});
imagePreviews.addEventListener("click", function (event) {
if (event.target.classList.contains("preview-del-btn")) {
var fileIndex = event.target.dataset.fileIndex;
var imagePreview = event.target.closest("#forUpload-ul > li");
imagePreview.remove();
fileArray.splice(fileIndex, 1);
fileArrayIndex--;
curImageCount--;
checkImg();
}
});
function checkImg(){
if(curImageCount > 0){
imgSubmitOn = true;
}else{
imgSubmitOn = false;
}
if(titleSubmitOn && imgSubmitOn){
$('#gallery_submit').css('background-color', 'coral');
$('#gallery_submit').attr("disabled", false);
}else{
$('#gallery_submit').css('background-color', '#ccc');
$('#gallery_submit').attr("disabled", true);
}
}
});
위 코드는 관련 처리를 하는 부분의 모든 코드를 가져온 것이다.
필요하면 참고하도록 하고 하나씩 보도록 하겠다.
우선 전역 변수를 선언해뒀는데, 설명은 아래와 같다.
let curImageCount = 0; // <input type = "file">에 현재 들어온 이미지 개수
let imgSubmitOn = false; // submit 버튼 제어를 위한 이미지 등록여부 체크 변수
let fileArray = []; // 등록된 이미지 저장 배열
두 번째 변수는 명확하므로 설명하지 않고 넘어가겠다.
첫 번째와 세 번째 변수를 굳이 왜 선언했나 싶을 수 있다.
<input type = "file" multiple>을 통해 파일을 넣어서 로그를 찍어보면 알 수 있는데, 여러 파일을 받을 수는 있지만 그게 누적되진 않는다.
<input> 태그의 type을 file로 설정한 경우 FileList라는 것을 내부적으로 갖게 되는데, 이 리스트에 파일이 0번, 1번, 2번... 이런식으로 저장된다.
누적되지 않는다는 말은 이미지를 한 번에 두 장 선택해서 넣으면 FileList 0번에 1개, 1번에 1개가 저장되지만,
한 번에 하나 선택해서 두 번 넣으면 두 번째로 넣은 파일만 FileList 0번에 저장되고 앞 파일은 지워진다는 의미다.
무엇이 문제인가 할 수 있겠지만,
예를 들어, 세 장의 사진을 넣었는데 한 장이 마음에 안들어 바꾸고 싶다면, 한 장만 삭제하는게 아니라 새로운 사진을 포함해 다시 세 장을 골라야된다는 것이다.
혹은 두 장을 넣었는데 한 장을 추가하고 싶다면, 한 장만 다시 넣는게 아니라 세 장을 다시 골라서 넣어야한다.
이런 부자연스러운(?) 과정을 피하기 위해 현재 누적된 이미지 개수와 그 이미지 정보를 담고있는 배열을 따로 선언해둔 것이다.
다음은 내부에 선언된 변수를 보자.
const upload = document.getElementById("upload");
const imagePreviews = document.getElementById("imagePreviews");
const uploadUL = document.getElementById("forUpload-ul");
let fileArrayIndex = 0;
let maxImageCount = 4;
위 세 가지는 JSP에 적어둔 태그들을 가져온 것이다.
fileArrayIndex는 나중에 한 장씩 삭제할 수 있도록 개별 파일에 인덱스를 부여하기 위해 선언해뒀다.
maxImageCount는 최대로 입력받을 파일 개수이다.
upload.addEventListener("change", function(){
let files = this.files;
curImageCount += files.length;
if(curImageCount > maxImageCount){
curImageCount = uploadUL.childElementCount;
alert(maxImageCount + '개까지만 등록 가능합니다.');
return;
}
if(uploadUL.childElementCount > maxImageCount-1){
alert(maxImageCount + '개까지만 등록 가능합니다.');
return;
}
//이하 코드 생략
<input type = "file">에 change 이벤트가 발생하면 먼저 this.files를 통해 FileList를 files 라는 변수에 담아줬다.
그리고 입력된 이미지 개수를 curImageCount에 더해 누적 수를 저장해뒀다.
조건문은 파일 개수를 확인해서 최대 등록 개수보다 크면 return하도록 했다.
맨 아래서 maxImageCount-1을 해준 이유는 <li>태그를 생성하기 전에 먼저 <ul>태그의 자식 개수를 센 것이라 그렇다.
for(let i = 0; i < files.length; i++){
let file = files[i];
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function(){
uploadUL.style.display = "";
let imageSrc = reader.result;
let imgLi = document.createElement("li");
uploadUL.appendChild(imgLi);
let selectedImg = document.createElement("img");
selectedImg.src = imageSrc;
selectedImg.className = "preview-img";
imgLi.appendChild(selectedImg);
let delBtn = document.createElement("input");
delBtn.type = "button";
delBtn.className = "preview-del-btn";
delBtn.value = "X";
delBtn.dataset.fileIndex = fileArrayIndex;
fileArrayIndex++;
imgLi.appendChild(delBtn);
fileArray.push(file);
}
}
checkImg();
});
다음은 반복문을 돌며 입력받은 파일 개수만큼 태그를 생성해주는 부분이다.
복잡하니 아래 결과 이미지를 보면 아래와 같다.
이미지는 이렇다.
FileReader 설명은 여기를 참고하자.
동적으로 태그를 생성하는 부분은 딱히 설명할 내용이 없다.
fileArrayIndex를 삭제버튼의 dataset 값으로 넣어주고 있고, fileArray에 파일을 하나씩 push하고 있다는 것만 알면 된다.
예를 들어, 총 파일 4개를 넣을 것인데 현재 두 개의 파일만 업로드했다면,
let files = this.files;
curImageCount += files.length;
위 과정에 의해 curImageCount는 2
for(let i = 0; i < files.length; i++){
//코드생략
delBtn.dataset.fileIndex = fileArrayIndex;
fileArrayIndex++;
//코드생략
위 과정에 의해 첫 번째 파일의 file-index는 0, 두 번째 파일의 file-index는 1
fileArray.push(file);
}
위 과정에 의해 fileArray의 0번 인덱스에 첫 번째 파일, 1번 인덱스에 두 번째 파일이 들어가 있는 것이다.
다른 어느 곳에서도 curImageCount와 fileArrayIndex를 초기화하지 않았으므로, 새로 파일이 input되더라도 현재 수를 유지하고 있는 상태다(누적하기 위해서).
여기서 각 파일의 fileArrayIndex와 fileArray에서 해당 파일이 갖는 인덱스가 일치한다.
이를 이용해서 아래 삭제과정을 진행할 것이다.
마지막 checkImg() 함수는 submit 버튼 제어를 위한 함수이므로 설명은 하지 않도록 하겠다.
다음은 등록한 이미지를 삭제하는 코드다.
imagePreviews.addEventListener("click", function (event) {
if (event.target.classList.contains("preview-del-btn")) {
var fileIndex = event.target.dataset.fileIndex;
var imagePreview = event.target.closest("#forUpload-ul > li");
imagePreview.remove();
fileArray.splice(fileIndex, 1);
fileArrayIndex--;
curImageCount--;
checkImg();
}
});
미리보기 이미지 영역인 imagePreviews에 클릭이벤트가 발생하면 클릭된 부분에 삭제버튼이 있는지 확인했다.
이후 fileIndex에 클릭된 타겟의 file-index(=fileArrayIndex)를 가져와 담아주고, CSS 선택자로 입력한 부분과 가장 가까운 요소를 찾아 imagePreview에 담아줬다.
이미지를 클릭하든 X버튼을 클릭하든 해당 요소(<li>)를 삭제해주기 위해 위와 같이 만들었다.
다음은 splice함수를 이용해 fileArray에서 클릭된 타겟의 인덱스 요소를 지워주고,
fileArrayIndex, curImageCount를 하나씩 감소시켰다.
이렇게 해주면 파일을 하나씩 네 번을 넣어도, 네 개를 동시에 넣어도, 넣었던 파일을 중간에 삭제하고 다시 넣어도 배열에 원하는 이미지가 남게된다.
일단 다중 이미지 파일 미리보기 기능을 정리해봤다.
대충 알고 있는 개념들로 내 마음대로 짰다보니 뭐 얼마나 좋은 코드겠냐마는 나중에 참고할 수 있길 바라며.. 마치도록 하겠다.