ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JavaScript] SpringBoot : 다중 이미지 Ajax 전송(Ajax DB 테이블 생성, Ajax promise, <form> + Ajax 데이터 전송)
    프로젝트/기능 정리 2023. 7. 24. 15:10

    목적

    • 다중 이미지 Ajax 전송 기능 구현
    • <form>데이터 + Ajax 데이터 동시에 서버 전송 처리
    • 비동기 방식 순차 처리를 위한 promise 사용

     

    이전 글에서 다중 이미지 미리보기 처리과정을 정리했다.

     

    폼을 다시 보면 아래와 같은데,

     

    폼 이미지

     

    텍스트와 이미지를 한번에 서버로 전송해야 하는 구성이다.

     

    이전 글과 같은 방식으로 이미지를 입력 받고, 다른 텍스트들이 채워지면 submit 버튼(게시)이 활성화 되도록 했다.

     

    submit을 하게되면 텍스트 데이터는 <form>태그에 의해 POST 방식으로, 이미지 배열은 Ajax에 의해 POST 방식으로 서버에 넘겨주도록 만들었다.

     

    하나씩 정리해보자.

     

    [JSP]

    우선 <form>태그 구조는 아래와 같다.

     

    <form:form> 관련 설명은 여기

    <form:form action = "write.do" method = "post" modelAttribute = "galleryVO" id = "gallery_register">
        <input type = "hidden" name = "g_num" id = "g_num">
        <textarea id = "gallery_content" name = "g_content" placeholder = "오늘의 추억을 기록해보세요"></textarea>
        <hr>
        <ul id = "select-section">
            <li>
                <img class = "option-icon" src = "../image_bundle/price.png">
                <span class = "option-title">제목</span>
            </li>
            <li class = "hide">
                <input type = "text" class = "input-textbox" name = "g_title" placeholder = "제목을 입력해주세요.">
            </li>
            <hr>
            <li>
                <img class = "option-icon" src = "../image_bundle/picture.png">
                <span class = "option-title">사진</span>
            </li>
            <li class = "hide">
                <%-- <label for = "openWindow">파일선택</label>
                <input type = "button" id = "openWindow" value = "파일선택" onclick = "window.open('${pageContext.request.contextPath}/FileInput.html','dd', 'location = no, status = no, width = 360px, height = 360px')" style = "display:none;"> --%>
                <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>
            <hr>
            <li>
                <img class = "option-icon" src = "../image_bundle/hash-key.png">
                <span class = "option-title">해시태그</span>
            </li>
            <li class = "hide">
                <input type = "text" class = "input-textbox" name = "g_hash" placeholder = "#해시태그 형태로 입력해주세요.">
            </li>
            <hr>
            <li>
                <img class = "option-icon" src = "../image_bundle/placeholder.png">
                <span class = "option-title">장소</span>
            </li>
            <li class = "hide">
                <input type = "text" id ="keyword" class = "input-textbox" name = "g_place" placeholder = "장소를 검색해주세요.">
            </li>
        </ul>
        <ul id = "placesList"></ul>
    </form:form>
    <div id="submit_btn">
        <button id="gallery_submit" disabled='disabled'>게시</button>
    </div>

    흐름을 보기 위해 적어두지만 따로 설명은 하지 않겠다. 위 태그들에 디자인을 입히면 위에 제시한 폼 이미지처럼 된다.

     

    [JavaScript]

    이전 글에서 이미지를 배열에 담는 과정을 설명했다(여기).

     

    간략히 코드를 다시 보면 아래와 같다.

    upload.addEventListener("change", function(){
        let files = this.files;
        curImageCount += files.length; // 지금 선택된 이미지 개수만큼 curImageCount+
    
        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를 이미지 index로 할당
                fileArrayIndex++; //for문이 돌아갈때마다 fileArrayIndex+
                imgLi.appendChild(delBtn);
    
                fileArray.push(file); //배열에 하나씩 push
            }
        }
        checkImg();
    });

    간단히 흐름만 보기 위해 파일 개수를 제한하는 코드는 제외했다.

     

    <input type = "file"> 태그에 이미지가 들어올 때마다 위 과정이 실행되는데, 흐름은 아래와 같다.

     

    input 과정 설명

     

    file input으로 받은 데이터가 누적되어 FileList에 저장되는 것이 아니므로 fileArray를 만들어 여러번 input해도 순서대로 저장되도록 했다.

     

    다음으론 fileArrayIndex 값과 실제 해당 파일이 fileArray에서 갖는 index 번호가 같도록하여 파일을 삭제할 수 있도록 했다.

     

    자세한 과정은 이전 글에 설명되어 있으니 필요하면 참고하도록 하자.

     

     

    이렇게 해서 이미지 파일을 등록하면 fileArray 라는 배열에 하나씩 데이터가 담기도록 해두었다.

     

    이제 JS에서 이렇게 받은 <form> 데이터와 fileArray 배열을 어떻게 서버로 전송하는지 보도록 하자.

     

    [JavaScript]

    기타 입력값 여부를 체크하고 submit 버튼을 활성화시키는 부분은 제하고 보도록 하겠다.

     

    흐름을 먼저 설명하도록 하겠다.

     

    보통 게시물을 등록하면 서버는 <form>에 담긴 데이터를 받아서 DB에 Insert하는 방식으로 테이블을 생성한다.

     

    근데 지금은 텍스트 데이터는 <form>으로, 이미지 데이터는 Ajax로 전달하는 방식을 취하고 있다(어떻게 하다보니 이렇게 됐다..).

     

    문제는 <form>이 먼저 전송되면, 컨트롤러가 return하는 view로 페이지 이동이 발생하고, Ajax 처리가 완료되지 않은 상태로 넘어가게된다.

     

    Ajax를 먼저 실행해서 이미지 데이터를 먼저 DB에 저장하면 되지 않는가 할 수 있는데 테이블 구조를 보면 이렇다.

     

    갤러리 테이블 관계도

     

    즉, 먼저 gallery 테이블이 생성되어 있어야 해당 key를 foreign key로 하여 img_gallery 테이블이 생성되는 것이다.

     

    이를 위해 크게 Ajax를 두 번 사용했다.

     

    먼저 글을 등록하는 회원의 회원번호(mem_num)만 가지고 gallery 테이블을 생성하는 Ajax를 실행하고, 그 리턴값으로 생성된 행의 번호(g_num, primary key)를 받아온다.

     

    다음으로 배열에 담긴 이미지를 하나씩 서버에 넘겨주는 Ajax를 실행했다.

     

    뭔가 굉장히 망한(?) 흐름 같은데.. 일단 이렇게 했으니까.. 마저 보도록 하자.

     

    먼저 상위 Ajax는 아래와 같다.

    $(function(){
        $('#gallery_submit').click(function(){
            $("input[name=upload]").attr("disabled", true);
            $.ajax({
                url : '../gallery/insertGallery.do',
                type : 'post',
                dataType : 'json',
                success : function(param){
                    if(param.result == 'successRow'){
                        var g_num = param.g_num;
                        sendImgArray1(fileArray, g_num);
                    }else if(param.result == 'failRow'){
                        console.log('행추가 안됨')
                    }
                }
            });			
        });
    });

    submit 버튼에 클릭 이벤트가 발생하면 실행되도록 했다.

     

    url에 매핑된 컨트롤러 메소드는 회원 번호만 저장된 새로운 행을 생성한다.

     

    gallery 행 생성 예시

     

    성공적으로 생성이 되었다면 서버에선 "successRow"라는 문자열과 생성된 g_num을 리턴한다.

     

    이렇게 받은 g_num과 이미지 배열을 sendImgArray1이라는 함수로 전달하는 것으로 상위 Ajax는 완료된다.

     

    다음으로 sendImgArray1 함수를 보자.

    function sendImgArray1(fileArray, g_num){
        if (fileArray.length == 0) {
            $('#g_num').val(g_num);
            $('#gallery_register').submit();
            return;
        } 
        var image_form_data = new FormData();
        image_form_data.append('upload', fileArray[0]);
        image_form_data.append('g_num', g_num);
        $.ajax({
            url: '../gallery/insertGalleryImage.do',
            data: image_form_data,
            type: 'post',
            dataType: 'json',
            contentType: false,
            processData: false,
            success: function(param) {
                if (param.result == 'success') {
                    fileArray.splice(0, 1);
                    sendImgArray1(fileArray, g_num);
                } else {
                    reject('이미지 추가 오류');
                }
            }
        });	
    }

     

    가장 먼저 fileArray의 길이를 체크해서 0인 경우, 즉 배열에 남은 파일이 없는 경우 <form> 데이터를 전송하도록 했다.

     

    왜 가장 먼저 저 조건을 체크하냐면.. 배열에 담긴 이미지를 하나씩 보내고, 보낸 이미지는 배열에서 삭제하는 방식으로 진행했기 때문이다.

     

    그래서 구성을 보면 FormData를 생성해서 fileArray의 0번째 요소와 g_num을 세팅해준 후, 서버에 데이터를 전송한다.

     

    성공적으로 서버에서 처리를 완료했다면 "success"라는 문자열을 보내오고,

     

    fileArray에서 0번째 요소를 삭제한 후 다시 sendImgArray1, 즉 자신을 다시 호출한다.

     

    재귀함수 형태로 만든 것인데, 당연히 반복적으로 메모리를 사용하므로 좋은 방식은 아니겠지만 달리 방법이 떠오르지 않아 이렇게 만들었다.

     

    이미지 파일 4개 정도는 4번 반복이므로 크게 느리진 않지만, 너무 많은 파일을 한번에 보낼 때는 다른 방법을 생각해봐야할 것 같다.

     

    컨트롤러와 매퍼, 서비스 코드는 흐름만 보도록 하겠다.

     

    [Mapper]

    @Insert("INSERT INTO gallery (mem_num) VALUES (#{mem_num})")
    public void insertGallery(Integer mem_num);
    
    @Select("SELECT g_num FROM gallery WHERE g_title IS NULL")
    public Integer getG_num();
    
    @Insert("INSERT INTO img_gallery (img_filename, img_file, g_num) VALUES (#{img_filename}, #{img_file}, #{g_num})")
    public void insertGalleryImg(GalleryImgVO galleryImg);
    
    @Update("Update gallery SET g_title = #{g_title}, g_content = #{g_content}, g_place = #{g_place}, g_cookie = #{g_cookie}, g_hash = #{g_hash} WHERE g_num = #{g_num}")
    public void insertGalleryContent(GalleryVO gallery);

     

    [Controller]

    @Controller
    public class GalleryController {
    
        @Autowired
        private GalleryService galleryService;
        
        @RequestMapping("/gallery/insertGallery.do")
        @ResponseBody
        public Map<String, Object> insertGalleryTable(HttpSession session){
    
            MemberVO user = (MemberVO)session.getAttribute("user");
    
            Map<String, Object> mapAjax = new HashMap<String, Object>();
    
            try {
                galleryService.insertGallery(user.getMem_num());			
    
                mapAjax.put("g_num", galleryService.getG_num());
                mapAjax.put("result", "successRow");
            }catch(Exception e) {
                mapAjax.put("result", "failRow");			
            }
            return mapAjax;		
        }
    
        @RequestMapping("/gallery/insertGalleryImage.do")
        @ResponseBody
        public Map<String, String> insertImage(GalleryImgVO galleryImg, HttpSession session){
    
            Map<String, String> mapAjax = new HashMap<String, String>();
    
            try {
                galleryService.insertGalleryImg(galleryImg);
    
                mapAjax.put("result", "success");
            }catch(Exception e) {
                mapAjax.put("result", "fail");			
            }
            return mapAjax;
        }
    }

    [Service]

    public interface GalleryService {
        public void insertGallery(Integer mem_num);
    
        public Integer getG_num();
    
        public void insertGalleryImg(GalleryImgVO galleryImg);
    
        public void insertGalleryContent(GalleryVO gallery);
    }

    [ServiceImpl]

    @Service
    @Transactional
    public class GalleryServiceImpl implements GalleryService{
    
        @Autowired
        private GalleryMapper galleryMapper;
        
        @Override
        public void insertGallery(Integer mem_num) {
            galleryMapper.insertGallery(mem_num);
        }
        
        @Override
        public Integer getG_num() {
            return galleryMapper.getG_num();
        }
    
        @Override
        public void insertGalleryImg(GalleryImgVO galleryImg) {
            galleryMapper.insertGalleryImg(galleryImg);
        }
        
        @Override
        public void insertGalleryContent(GalleryVO gallery) {
            galleryMapper.insertGalleryContent(gallery);
        }
    }

     

    [Ajax promise]

    이미 위 방식으로 모두 작성한 후에 어느 외국 유튜버의 영상을 보다가 promise라는 것을 알게되었다.

     

    Ajax나 setTimeout 같이 JS에서 비동기 방식으로 처리되는 구문들은 코드를 적은 순서대로 실행되는 것이 아니다.

     

    특정 조건 이후에, 혹은 무언가 데이터를 전달 받은 후와 같이 비동기 처리간 순서가 필요할 때 promise를 사용할 수 있는 듯 하다(영어를 다 못알아들음).

     

    뭐 API 데이터를 다 불러온 후 어떤 처리를 해야한다거나 할 때도 사용할 수 있다.

     

    promise인 이유는 이런식으로 생각하면 될 것 같다.

     

    '지금은 내가 줄 데이터가 없는데, 일단 이 계약서(약속)를 받아. 그게 오면 내가 꼭 넘겨주도록 약속할게.' 정도..

     

    아무튼 위 과정을 진행하면서 Ajax 실행 순서로 좀 애를 먹었는데, 나중에 어찌 사용할지 모르니 변형해보자 싶어서 해봤다.

     

    일단 원하는건 재귀함수를 사용하지 않고

     

    1. 테이블 행 생성 Ajax 실행
    2. 행 생성 완료 후 이미지 개수만큼 동시에 Ajax 실행해서 서버에 이미지 전달

     

    위 과정을 진행하는 것이었으나, 재귀함수는 그대로 사용해버렸다..

     

    일단 구조를 적어두고 나중에 디벨롭 해보도록 하자..

     

    $('#gallery_submit').click(function(){
        $("input[name=upload]").attr("disabled", true);
        promiseInsertGallery()
                    .then(sendImgArray)
                    .catch(onError)
    });

    먼저 submit 버튼이 클릭됐을 때 발생하는 이벤트 부분이다.

     

    길다란 ajax 구문이 사라져 보기 좋아졌다.

     

    promiseInsertGallery라는 함수가 먼저 실행되고, 완료되면 .then(sendImgArray)가 실행, 중간에 오류 발생시 건너뛰고 .catch(onError)가 실행되는 순서다.

     

    function promiseInsertGallery(){
        return new Promise(function(resolve, reject) {
            $.ajax({
                url: '../gallery/insertGallery.do',
                type: 'post',
                dataType: 'json',
                success: function(param) {
                    if (param.result == 'successRow') {
                        resolve([fileArray, param.g_num]);
                    } else if (param.result == 'failRow') {
                        reject("행 추가 오류");
                    }
                }
            });
        });
    }

    primiseInsertGallery 함수는 위와 같다.

     

    똑같은 Ajax 구문인데 해당 내용을 promise에 담아줬다.

     

    실행완료되면 그 성공 여부에 따라 resolve 또는 reject가 반환된다.

     

    function sendImgArray([fileArray, g_num]){
        if (fileArray.length == 0) {
            $('#g_num').val(g_num);
            $('#gallery_register').submit();
            return;
        } 
        var image_form_data = new FormData();
        image_form_data.append('upload', fileArray[0]);
        image_form_data.append('g_num', g_num);
        $.ajax({
            url: '../gallery/insertGalleryImage.do',
            data: image_form_data,
            type: 'post',
            dataType: 'json',
            contentType: false,
            processData: false,
            success: function(param) {
                if (param.result == 'success') {
                    fileArray.splice(0, 1);
                    sendImgArray([fileArray, g_num]);
                } else {
                    reject('이미지 추가 오류');
                }
            }
        });	
    }

    첫 함수에서 resolve가 반환되었다면, 해당 함수의 진행이 끝난 것으로 .then에 등록된 함수가 이어서 실행된다.

     

    놀라운(?) 점은 위에 실행 구문을 보면 .then(sendImgArray)라고 적혀있고 어떤 인자도 넘겨주지 않고 있다.

     

    그런데 sendImgArray는 ([fileArray, g_num]) 이 두 가지 인자를 받아야하는 것으로 선언되어 있다.

     

    이게 promise가 해주는 장점 중 하나인데, 실행완료 후 resolve에 담긴 데이터들을 다음 실행되는 함수의 파라미터로 알아서 넣어준다고 한다.

     

    중간에 어떤 과정에서 reject를 반환하면 catch에 잡혀서 error 함수로 이동하게 되는데 해당 함수는 아래와 같다.

    function onError(err){
        console.log(`Error : ${err}`);
    }

    단순히 콘솔에 에러 문구를 띄워주는 것으로 했다.

     

    사실 이렇게 하나 처음 방식대로 하나 전송 속도는 비슷했다.

     

    각 5번씩 테스트해서 평균내본 값인데, 

     

    대략 DB생성 Ajax: 둘다 36.6ms / 각 이미지 전송 Ajax: 원래방식 - 61.75ms,  promise - 57.2ms 정도였다.

     

    그럴 수밖에 없는게 둘의 구조가 사실상 동일하다.

     

    또 더 중요한건 각 이미지 전송 속도가 아니라 전체 이미지 전송에 걸리는 시간인데,

     

    재귀적으로 처리하면 하나씩 전송되므로 대충 61.75ms × 4 / 57.2ms × 4의 시간이 걸린다.

     

    가장 좋은 방법은 첫 Ajax를 먼저 실행하고, 완료 후 나머지 이미지 전송은 비동기적으로 처리하는 것이 되겠다.

     

    당장은 어떻게 효율적으로 작성할 수 있을지 떠오르지 않아서 여기까지 정리하는 것으로 하고.. 다음에 필요하면 다시 도전해보겠다.

Designed by Tistory.