스프링과 JPA 기반 웹 애플리케이션 개발 #31 프로필 이미지 변경 (+FileReader.readAsDataURL() +Base64 인코딩이란? +DataURL이란?)

Jake Seo·2021년 6월 3일
0

스프링과 JPA 기반 웹 애플리케이션 개발 #31 프로필 이미지 변경

해당 내용은 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발의 강의 내용을 바탕으로 작성된 내용입니다.

강의를 학습하며 요약한 내용을 출처를 표기하고 블로깅 또는 문서로 공개하는 것을 허용합니다 라는 원칙 하에 요약 내용을 공개합니다. 출처는 위에 언급되어있듯, 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발입니다.

제가 학습한 소스코드는 https://github.com/n00nietzsche/jakestudy_webapp 에 지속적으로 업로드 됩니다. 매 커밋 메세지에 강의의 어디 부분까지 진행됐는지 기록해놓겠습니다.


프로필 이미지 변경

  • 이미지 잘라서 저장하는 기능
    • 프론트 라이브러리 필요
      • Cropper.js
      • npm install cropper
      • npm install jquery-cropper
  • Cropper.js 사용법
$("#profile-image-file").change(function(e) {
  if (e.target.files.length === 1) {
    const reader = new FileReader();
    reader.onload = e => {
      if (e.target.result) {
        let img = document.createElement("img");
        img.id = "new-profile";
        img.src = e.target.result;
        img.width = 250;
        
        $newProfileImage.html(img);
        $newProfileImage.show();
        $currentProfileImage.hide();
        
        let $newImage = $(img);
        $newImage.cropper({aspectRatio: 1});
        cropper = $newImage.data('cropper');
        
        $cutBtn.show();
        $confirmBtn.hide();
        $resetBtn.show();
      }
    };
    
    reader.readAsDataURL(e.target.files[0]);
  }
});
  • DataURL이란?
    • data: 라는 접두어를 가진 URL로 파일을 문서에 내장시킬 때 사용할 수 있다.
    • 이미지를 DataURL로 저장할 수 있다.

프로필 이미지 변경

이번 편은 사실 백엔드와 크게 연관있는 편은 아니었다.

프로필 업데이트 관련

기존에 기본 정보들은 업데이트하는 것이 이미 다 구현되어 있어서, 이미지만 좀 새로 추가했다.

Package.json 패키지 추가

npm install cropper
npm install jquery-cropper

두개를 추가했다.

Fragments.html 소스 변경

  <!-- Font Awesome CSS-->
  <link rel="stylesheet" href="/static/node_modules/font-awesome/css/font-awesome.min.css">

  <!-- cropper CSS-->
  <link rel="stylesheet" href="/static/node_modules/cropper/dist/cropper.min.css">

  <!-- 부트스트랩 JS -->
  <script src="/static/node_modules/jquery/dist/jquery.js"></script>
  <script src="/static/node_modules/bootstrap/dist/js/bootstrap.bundle.js"></script>
  <!-- jdenticon-->
  <script src="/static/node_modules/jdenticon/dist/jdenticon.min.js"></script>

  <!-- cropper JS-->
  <script src="/static/node_modules/cropper/dist/cropper.min.js"></script>
  <script src="/static/node_modules/jquery-cropper/dist/jquery-cropper.min.js"></script>
          <svg th:if="${#strings.isEmpty(account?.profileImage)}"
               width="24" height="24" data-jdenticon-value="user127"
               th:data-jdenticon-value="${#authentication.name}"
               class="rounded border bg-light"></svg>
          <img th:if="${!#strings.isEmpty(account?.profileImage)}"
               th:src="${account.profileImage}"
               width="24" height="24" class="rounded border" />

cropper에 사용될 css와 js를 추가했고, 프로필 사진이 있다면 jdenticon 대신에 profileImage를 쓰도록 했다.

settiongs/profile.html 소스 추가

<script type="application/javascript">
    $(function () {
        let cropper = '';

        let $confirmBtn = $("#confirm-button");
        let $resetBtn = $("#reset-button");
        let $cutBtn = $("#cut-button");
        let $newProfileImage = $("#new-profile-image");
        let $currentProfileImage = $("#current-profile-image");
        let $resultImage = $("#cropped-new-profile-image");
        let $profileImage = $("#profileImage");

        $newProfileImage.hide();
        $cutBtn.hide();
        $resetBtn.hide();
        $confirmBtn.hide();

        $("#profile-image-file").change(function (e) {
            if (e.target.files.length === 1) {
                const reader = new FileReader();
                reader.onload = e => {
                    if (e.target.result) {
                        let img = document.createElement("img");
                        img.id = "new-profile";
                        img.src = e.target.result;
                        img.width = 250;

                        $newProfileImage.html(img);
                        $newProfileImage.show();
                        $currentProfileImage.hide();

                        let $newImage = $(img);
                        $newImage.cropper({aspectRatio: 1});
                        cropper = $newImage.data('cropper');

                        $cutBtn.show();
                        $confirmBtn.hide();
                        $resetBtn.show();
                    }
                };

                reader.readAsDataURL(e.target.files[0]);
            }
        });

        $resetBtn.click(function () {
            $currentProfileImage.show();
            $newProfileImage.hide();
            $resultImage.hide();
            $resetBtn.hide();
            $cutBtn.hide();
            $confirmBtn.hide();
            $profileImage.val('');
            $("#profile-image-file").val('');
        });

        $cutBtn.click(function (e) {

            let dataUrl = cropper.getCroppedCanvas().toDataURL();
            let newImage = document.createElement("img");
            newImage.id = "cropped-new-profile-image";
            newImage.src = dataUrl;
            newImage.width = 125;
            $resultImage.html(newImage);
            $resultImage.show();
            $confirmBtn.show();

            $confirmBtn.click(function () {
                $newProfileImage.html(newImage);
                $cutBtn.hide();
                $confirmBtn.hide();
                $profileImage.val(dataUrl);
            })
        })
    });
</script>

버튼 및 이미지 뷰의 구성

구성요소적으로 살펴보면 먼저 3개의 버튼(자르기, 확인, 취소)과 4개의 이미지 공간(현재 프로필 이미지, 새로운 프로필 이미지, 자르기 위한 뷰박스의 프로필 이미지, 잘린 후의 프로필 이미지)이 있다.

Input 박스의 활용과 DataURL

Input 태그를 단순히 form에 데이터를 전송하는 용도로 사용한 것이 아니라, 실제 사용자에게 보여주기 위해서도 사용했다.

원래 type=file로 지정된 input은 파일을 첨부하기 위해서, 내부적으로 files라는 프로퍼티에 FileList라는 객체를 이용하여 파일들을 보관했다가, formaction이 향하는 곳으로 파일을 전달한다. 보통 enctype="multipart/form-data" 라는 속성을 함께 더해준다.

일반적으로 formaction이 가리키는 경로에서는 MultipartFile이라는 타입으로 해당 파일을 받는 컨트롤러가 대기하고 있다가 파일을 받는다.

그런데 위 코드는 약간 다른 방법을 쓴다. reader라는 변수에 FileReader() 클래스를 생성하여 이용한다. readerinput.files에 있는 FileList 내부 File 객체들을 .readAsDataURL() 메소드를 통해 읽어낼 수 있다. 이미지를 읽어내면 해당 이미지는 data:image/png;base64,.....과 같은 형식의 문자열로 변한다.

이 방식을 이용하면 사용자가 방금 첨부한 그림을 바로 html에 임베딩시켜서 보여줄 수 있다. img 태그 내부의 src 속성에 data:image/png;base64,... 문자열을 그대로 넣으면 해당 이미지가 임베딩되어 보인다.

FileReader.readAsDataURL() 설명

readAsDataURL() 메소드는 Blob이나 File으로 세부화된 내용을 읽을 때 사용된다. 읽는 작업이 끝나면, readyStateDONE이 되고, loadend가 트리거된다. 그 때, result 애트리뷰트는 data:URL(파일의 내용을 base64로 인코딩한 문자열)을 갖게 된다.

loadendload 이후에 트리거되는 이벤트이다.

Data URLs 란?

data: 스키마로 시작하는 URL을 말한다. 컨텐츠 생산자가 작은 파일을 도큐먼트에 임베딩 시키는 것을 허락한다. "data URIs" 로 많이 알려져 있었지만, 이제는 그 단어를 쓰지 않는다.

데이터 URL은 탐색을 담당하는 설정 객체의 출처를 상속받는 것보다도 최신 브라우저에 의해 고유한 원본으로 취급받는다.

Base64 인코딩이란?

데이터를 radix-64(64진법) 표현식으로 해석해 Character set에 영향을 받지 않는 공통 ASCII 문자열 포맷으로 된 바이너리 데이터를 표현하는 binary-to-text 인코딩 스키마의 집합이다. (더 자세하게는 8bit bytes의 시퀀스로 표현하는 집합)

요약하자면, 64진법을 이용해 Binary Data를 Text로 변경하는 Encoding이다.

위의 테이블에 있는 문자들을 이용한다.

나머지 로직

새로운 이미지가 들어오면 $("#profile-image-file").change() 현재 프로필 이미지를 숨기고 새로운 프로필 이미지를 보여준다. 새 이미지 태그를 기반으로 새이미지.cropper() 메소드를 수행한다. .cropper() 메소드는 jquery-cropper 에 의해 jquery 객체 내장 메소드가 된 상태이다.

__proto__cropper() 내장 메소드가 보인다. 옵션을 입력할 수 있는데, {aspectRatio: 1}정도만 설정해주었다. 새 이미지가 들어오면, 자르기 버튼과 취소 버튼이 활성화되고, 확인 버튼은 숨겨진다.

자르기 버튼을 누르면 cropper.getCroppedCanvas().getDataURL()을 한 결과가 새 img 태그의 src에 들어가게 된다.

위는 croppedCanvas.toDataURL()의 결과인데, 용량이 반으로 줄어있는 것을 볼 수 있다.

결국 그 이미지가 $resultImage()로 들어간다. 그리고 확인 버튼을 누르면 hidden 타입으로 존재하던 profileImage라는 id를 가진 인풋에 들어가고 그게 컨트롤러로 전해지고 결국 그 문자열은 DB에 저장되어 해당 유저의 프로필 이미지를 보여줄 때 쓰인다.

profile
풀스택 웹개발자로 일하고 있는 Jake Seo입니다. 주로 Jake Seo라는 닉네임을 많이 씁니다. 프론트엔드: Javascript, React 백엔드: Spring Framework에 관심이 있습니다.

0개의 댓글