[Jquery] ajax를 이용해 파일 업로드하는 방법


안녕하세요. 명월입니다.


이 글은 ajax를 이용해서 웹 서버로 파일을 전송하는 예제입니다.


제가 이 글을 예전에 쓴 적이 있는 줄 알았는데 없네요.. 그래서 다시(?) 작성합니다.

구글에서 ajax로 파일 업로드를 찾으면 보통 input file에서 form 데이터로 변환하고 enctype를 multipart/form-data로 설정해서 submit하면 서버로 보낼 수 있다라는 글이 대부분입니다.

사실 틀린 이야기는 아닙니다만 굳이 submit으로 보낼꺼면 ajax를 이용할 필요가 없을 듯 싶네요. 그냥 웹 request로도 똑같은 결과가 나올 텐데..


많은 사람들은 구글이나 외국 사이트에서 파일을 업로드하면 프로그래스바로 업로드 상태를 확인하면서 업로드 되는 것을 아마 ajax로 구현하고 싶다라는 생각으로 찾을 듯 싶은데 그런 방법은 없는 듯 해서 제가 작성합니다.


기본적으로 쉽게 생각하면 브라우져에서 자바스크립트로 로컬 파일을 건드리는 건 브라우저 세큐리티에 위배되는 것이라 아마 옛날 IE8부터 금지되었습니다.

그렇다고 방법이 없나? 자바스크립트의 FileReader 객체를 이용하면 binary나 base64로 변환해서 사용할 수 있습니다.


FileReader는 아마 옛날 IE8의 경우는 작동을 안 할 수 있습니다만 최신 브라우저는 다 됩니다.

<input type="file" id="fileupload">
<button id="uploadClick">파일 업로드</button>
<span id="progress"></span>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script>
  $('#uploadClick').on('click', function() {
    var file = $("#fileupload")[0].files[0];
    var filename = file.name;
    var reader = new FileReader();
    reader.onload = function(e) {
      // 변환이 끝나면 reader.result로 옵니다.
      var base64data = reader.result;
      console.log(base64data);
      // 여기서 구조가 중요합니다.
      // 구조는 「data: 파일 타입; base64, 데이터」입니다.
      var data = base64data.split(',')[1];
      //data가 이제 데이터 입니다.
      //사실 ajax로 넘길때는 큰 사이즈 설정해서 데이터를 넘기면 빠르게 되는데 
      //예제이다보니 프로그래스바 구조를 나타내기 위해 문자 1개 단위로 보내겠습니다.
      var sendsize = 1024;
      var filelength = data.length;
      var pos = 0;
      var upload = function() {
        $.ajax({
          type : 'POST',
          dataType : 'json',
          data : {
            filename : filename,
            filelength : filelength,
            filepos : pos,
            data : data.substring(pos, pos + sendsize)
          },
          url : './upload',
          success : function(data) {
            // 전체가 전송될 때까지
            if (pos < filelength) {
              // 재귀
              setTimeout(upload, 1);
            }
            pos = pos + sendsize;
            if (pos > filelength) {
              pos = filelength;
            }
            $('#progress').text(pos + ' / ' + filelength);
          },
          error : function(jqXHR, textStatus, errorThrown) {
          },
          complete : function(jqXHR, textStatus) {
          }
        });
      };
      setTimeout(upload, 1);
    }
    // base64로 넘깁니다.
    reader.readAsDataURL(file);
  });
</script>

위 소스를 보면 「파일 업로드」버튼을 누르면 위 함수가 호출이 됩니다. 여기서 FileReader를 호출해서 base64로 넘기면 onload 이벤트가 호출이 되는데 base64구조로 데이터가 옵니다.

참고로 readAdDataURL는 base64구조 입니다만「readAsArrayBuffer」는 바이너리 형식이고 「readAsText」는 스트링 형식입니다.

보시면 제가 이미지를 하나 등록을 하면 위와 같은 형식의 데이터가 나옵니다. 여기서 우리가 필요한 부분은 데이터 부분입니다. base64영역에서는 ,를 사용하지 않기 때문에 ,로 문자 분할하면 됩니다.


그리고 base64를 분할해서 서버로 전송하면 전송이 됩니다.

간단한 예제이기 때문에 java sevlet으로 서버를 작성하겠습니다.

@WebServlet("/upload")
public class upload extends HttpServlet {
  private static final long serialVersionUID = 1L;

  public upload() {
    super();
  }
  // cache 타입
  private class Node {
    String filename;
    int filepos;
    int filelength;
    StringBuffer data;
    Date lastupdated;
  }
  
  private static List<Node> memory = null;
  
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  
    // flyweight 타입
    if (memory == null) {
      memory = new LinkedList<>();
      // cache 가비지 컬렉션
      Executors.newSingleThreadExecutor().execute(() -> {
        while (true) {
          try {
            synchronized (memory) {
              for (int i = 0; i < memory.size(); i++) {
                Date now = new Date();
                // 최종 업데이트가 10초 이상 되면 파일 업데이트 실패로 간주, 제거한다.
                if (now.getTime() - memory.get(i).lastupdated.getTime() > 10000) {
                  System.out.println("제거!!");
                  memory.remove(i);
                  i--;
                }
              }
            }
            // 10초 단위로 실행
            Thread.sleep(10000);
          } catch (Throwable e) {
            e.printStackTrace();
          }
        }
      });
    }
  
    Node node = null;
    // 파라미터 타입
    String filename = request.getParameter("filename");
    String filelength = request.getParameter("filelength");
    String filepos = request.getParameter("filepos");
    String data = request.getParameter("data");
    // cache에 같은 filename의 업데이트 등록 내용이 있나 확인, 없으면 생성한다.
    synchronized (memory) {
      // 검색
      for (Node n : memory) {
        if (n.filename.equals(filename)) {
          node = n;
          break;
        }
      }
      // 생성
      if (node == null) {
        memory.add(node = new Node());
        node.filename = filename;
        node.data = new StringBuffer();
      }
    }
  
    // 업데이트 정보 등록
    node.filelength = Integer.parseInt(filelength);
    node.filepos = Integer.parseInt(filepos);
    node.lastupdated = new Date();
    
    // base64를 연결해서 등록한다.
    node.data.append(data);
    
    // 입력 위치와 파일 사이즈가 넘어서면 파일을 만든다.
    if (node.filepos >= node.filelength) {
      // cache에서 제거한다.
      synchronized (memory) {
        memory.remove(node);
      }
      // base64 타입의 문자열을 byte 배열의 binary로 변경
      byte[] binary = Base64.getDecoder().decode(node.data.toString());
      // 그대로 파일로 생성한다.
      try (FileOutputStream stream = new FileOutputStream("d:\\work\\" + filename)) {
        stream.write(binary, 0, binary.length);
      }
      response.getWriter().write("{\"ret\":\"complete\"}");
    } else {
      response.getWriter().write("{\"ret\":\"continue\"}");
    }
  }
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    doGet(request, response);
  }
}

사실 파일 업로드는 ajax보다는 웹소켓이 더 어울리기는 하는데 주제가 ajax이니 주제에 맞게 했습니다.

서버에서는 일단 파일 업로드가 끝나야 파일로 변환을 할수 있습니다. 그러기 때문에 캐쉬(cache)를 만들어서 파일 정보가 오면 차곡차곡 쌓습니다. ajax에서는 중간에 업로드가 끊길 경우 끊긴 여부를 알수가 없기 때문에 가비지 컬렉션을 만들어서 업로드가 완료되지 않았는데 중간에 끊길 경우 timeout으로 소거하는 방식으로 만들었습니다.


최종적으로 파일 업로드가 끝나면 base64를 바이너리 byte[]형식으로 바꾸고 저장하면 됩니다.

 byte[] binary = Base64.getDecoder().decode(node.data.toString());

이렇게 작성할 경우, 따로 웹 프로토콜 설정이나 formdata를 변경할 필요 없이 순수 ajax로 파일 업로드가 완료되었습니다. 제 생각에는 이게 ajax로 파일 업로드하는 방식이 가장 맞지 않나 싶네요..

enctype를 multipart/form-data으로 보내는 방법은 틀린 방법은 아닙니다만 설정이 많아서 귀찮습니다.


다음은 실행 화면입니다.

먼저 파일을 업로드 합니다.

파일이 업로드 완료가 되었는데 잘 저장이 되었는지 확인했습니다.

업로드가 잘 되서 파일이 잘 생성이 되었네요...


예제 파일을 첨부합니다.

WebExample.zip