이미지 URL 유효성 이중 검증과 User Mistake-Proofing 연습

Wonkook Lee·2021년 8월 16일
27

Toy Projects

목록 보기
2/4
post-thumbnail

이미지 URL 유효성 이중 검증과 User Mistake-Proofing 연습


Get Image Background | Exercise

썸네일 메이커의 개선 과정 중 이미지 URL의 적확한 유효성 검증 방법을 공부하기 위해 이번에도 작은 예제를 만들었다.

내가 만든 예제 링크 : Get Image Background




🛃 유효성 검증이란?

유효성 검증(Validation)은 처리할 데이터가 부정확, 불완전 또는 불합리한지 확인하기 위해 사용되는 처리 과정을 말한다.

우리는 일상에서 많은 유효성 검증을 경험한다. 대표적으로 웹페이지에서 회원 가입할때 입력한 전화번호와 이메일 주소가 해당 포맷인지, 사용자가 누락하거나 잘못 기입한 정보는 없는지 확인하는 절차를 겪어봤을 것이다.

웹페이지가 정상적으로 동작하기 위해선 허용된 데이터만 전달되어야 한다. 유효성 검증은 마치 공항의 검색대와 같다. 올바른 유효성 검증 없이 무작위로 데이터를 받아들이는 것은 공항 검색대를 거치지 않은 신원미상의 승객을 비행기에 태우는 것과 같다.




🤔 새로 추가되는 기능에 대한 고민

썸네일 메이커의 배경 생성과 관련하여 아래 두 가지 기능으로 나누어 추가될 예정이다.

  • 랜덤 배경 이미지 생성 : Unsplash에서 특정 범주의 이미지를 받아오는 기능
  • 클립보드 주소 붙여넣기 : Prompt 창에 사용자가 직접 이미지 주소를 붙여넣을 필요 없이 주소를 복사해서 버튼을 누르면 클립보드의 이미지 주소를 참조하여 자동으로 배경이 바뀌는 기능

그리고 텍스트의 주목성을 높이기 위해 배경을 어둡게 하거나, 흐리게 만들 수 있도록 옵션을 넣을 예정이다. 컨트롤 패널을 복잡하게 할 수도 있기 때문에 배경의 세부 조정은 별도의 UI를 사용할 예정이다.

랜덤 배경 이미지 가져오기는 앞서 포스팅한 예제에서 자세히 살펴볼 수 있다.




🤜🏻🤛🏻 쟁점

랜덤 배경 이미지는 정해진 주소값을 재차 요청하면 되어 별도의 유효성 검증이 필요없다.
하지만 사용자가 클립보드로부터 이미지 주소를 붙여넣을땐 prompt에 직접 입력하는 것 처럼 보여지지도 않고, 예상한 결과가 나오지 않을 경우 사용자는 뭐가 잘못되었는지 직관적으로 알기 어렵다.

보통 사용자는 "내가 무언가 잘못 입력했겠구나" 보다는 "이거 안되네"로 생각하는 경향이 있어서 최악의 경우 사이트를 이탈하게 된다.

따라서 클립보드 텍스트 기능은 prompt를 사용할 때보다 더 엄격한 유효성 검증 절차를 두고, 실패할 경우 뭐가 잘못되었는지 사용자에게 알려줘야 한다.

사용자가 입력한 값이 URL이 맞는가? 맞다면 URL이 이미지의 주소가 맞는가?

입력값이 검증되기 위해선 아래 두 가지 조건이 충족되어야 한다.

  • 입력값이 URL 형식이어야 하며 빈 문자열, 다른 문자열이 전달되어선 안된다.
  • 입력된 URL로 가져온(fetch(URL)) 데이터 형식은 이미지(image)만 허용한다.

따라서 문자열의 URL 여부를 검증하는 첫번째 유효성 검증(Validation#1)URL로 가져온 데이터가 이미지인지 확인하는 두번째 유효성 검증(Validation#2)이 필요하게 된다.




🧐 검증 #1 - 문자열이 URL 형식인가?

😎 raejoonee 님의 솔루션 - 정규표현식

첫번째 검증 절차는 너무나 감사하게도 raejoonee님께서 PR로 개선의견을 보내주셨다. (다시 한 번 감사드립니다 😢)

핵심 코드는 아래와 같다.

const imageBackground = function () {
  const regex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(:[0-9]+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
  let imgUrl = prompt('이미지 주소를 입력하세요 😇');
  if (imgUrl === null) return; // 취소 눌렀을 때 알림이 발생하지 않도록 수정 2021/08/12
  if (!imgUrl.match(regex)) {
    // 유효하지 않은 주소를 입력했을 때 알림 발생 2021/08/13
    alert('올바르지 않은 URL입니다 😨');
    return;
  }
};

URL 형식을 골라내기 위한 정규표현식인데, 도움받은거고 나는 아직 정규표현식을 다룰 줄 모른다.
따라서 해석은 나중에 제대로 이해하게 된다면 다시 포스팅 수정할 예정이다.

아무튼 위 솔루션을 참고한다면 전달된 문자열이 URL 형식이 아닐 경우 자동으로 걸러지게 되고 Denial 액션을 취할 수 있다.

위에서 알 수 있듯 URL 형식이 아닌 텍스트는 모두 걸러지게 되고, 미리 Validation으로 함수의 작동을 차단하여 불필요한 리소스 사용을 예방할 수 있다.

아래는 내가 적용한 Validation#1의 코드이다. navigator API를 사용하여 가져온 클립보드의 문자열이 정규표현식과 매칭된다면 이미지를 호출하는 절차인 getImage()를 수행하게 된다.

  // 유효성 검사 1 (문자열 검사)
  const validTxt = function(target) {
    const regex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(:[0-9]+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
    navigator.clipboard.readText()
    .then((text) => {
      if (text.match(regex)) {
        getImage(text, target);
      } else {
        errorBtn(target);
        setTimeout(() => {
          resetBtn(target);
        }, 1000);
      }
    })
  }

내가 좋아하는 깨알같은 디테일 넣기




🌃 검증 #2 - URL로 가져온 데이터가 이미지인가?

이 과정에서 많은 시간을 할애해 고민하게 되었다. fetch 요청하여 가져온 객체에서 어떤 프로퍼티를 참조해야 이것이 이미지임을 알 수 있을까? 전달받은 객체가 이미지임을 알 수 있는 특정 프로퍼티가 있을 것 같은데, 아무리 찾아봐도 감이 잡히질 않았고, 결국 메소드에 인자를 넣어 값을 받을 수 있게 되었다.

내가 찾은 정답부터 말하자면 아래와 같다. 찬양하라

Headers.get()

fetch 결과로부터 전달받은 객체를 response라고 한다면 response.headers.get('Content-Type') 메소드를 사용했다. 아래 코드 블럭에서 headers.get에 접근 전 프로퍼티의 부재로 다운되는 것을 막기 위해 옵셔널 체이닝(Optional Chaining)을 사용했지만 이 또한 undefined가 출력될 것이기에 추후 수정이 필요하다.

  // 유효성 검사 2 (컨텐츠 타입 검사)
  const validType = function(source) {
    const contentType = source?.headers?.get('Content-Type');
    return contentType.includes('image') ? true : false;
  }

아래 예시를 보면 클립보드에서 붙여넣은 주소의 타입에 따라 각기 다른 값을 반환받는 것을 알 수 있다.

JPEG 이미지는 image/jpeg로, GIF 이미지는 image/gif로 나오는 것을 알 수 있다.
또한 이미지가 아닐 경우 에러와 함께 해당 문서의 타입(text/html; charset-UTF-8)을 출력한다. 에러는 catch로 핸들링을 해놨지만 서버의 엑세스 에러는 핸들링할 수 없었다. 하지만 에러가 출력되어도 작동되는데는 문제가 없다.

따라서 나는 기쁜 마음으로 내가 원하는 로직을 완성할 수 있었다.

  // 이미지 요청하기
  const getImage = function(url, target) {
    target.setAttribute('disabled', true);
    target.classList.remove('hover');
    target.textContent = '이미지 가져오는 중...'
    
    // HTTP 요청
    fetch(url)
    .then((response) => {
      if(!validType(response)) {
        return;
      }
      return response;
    })
    .then((response) => {
      document.body.style.backgroundImage = `url(${response.url})`;
      return response;
    })
    .then(() => {
      setTimeout(() => {
        resetBtn(target);
      }, 600);
    })
    .catch(() => {
      target.removeAttribute('disabled');
      errorBtn(target);
      setTimeout(() => {
        resetBtn(target);
      }, 1000);
    })
  };



✋🏻 또 다른 개선 - 로딩 중 중복 요청 방지

이미지를 로드하는 도중에 유저가 버튼을 중복해서 클릭하지 못하도록 막아야 한다. 만약 계속해서 버튼을 누를 수 있게 허용한다면 불필요한 요청이 발생된다.

이는 <button>disabled 속성을 사용하면 해결할 수 있다.
버튼을 클릭하여 이미지를 요청하고, 이미지가 도착해서 렌더링 된다면(then) 그때 다시 disabled 속성을 태그에서 제거해주면 된다. (개인적으로 전역 플래그보다 더 효과적인 것 같다)

현재 이미지가 로드되고 있다는 것을 사용자에게 알려줘야하기 때문에 버튼에 간단한 로더 애니메이션을 넣었다. 그리고 완전히 로드 되더라도, 창이 번쩍이는 찰나가 있기 때문에 setTimeOut으로 disabled 속성이 제거될 때까지 500~700ms의 타이밍을 준다.

아래는 결과

이로써 사용자의 무자비한 중복 클릭을 예방할 수 있다.

외국의 제품 사용 설명서 중 온갖 기상 천외한 DO NOT 예시를 본적이 있을 것이다. 다리미로 고양이를 다리지 마시오
사용자가 극한의 악동이라고 가정하고 최대한 제품과 서버에 부하가 가지 않도록 미리 예방하는것도 좋은 습관인 것 같다.




글과 이미지
Wonkook Lee ⓒ All Rights Reserved

👉🏻 완성 예제 구경하기

Thank you.

🙏🏻 잘못된 정보가 있다면 지적해주세요

profile
© 가치 지향 프론트엔드 개발자

4개의 댓글

comment-user-thumbnail
2021년 8월 16일

글을 너무 잘 쓰셔서 술술 잘 읽었습니다.
그리고 디자인적 감각이 있으신 건지 유효하지 않은 주소를 알려줄 때나, 로더를 보여줄 때 css 적용된 모습이 너무 이쁘네요. 잘 봤습니다.

1개의 답글
comment-user-thumbnail
2021년 8월 17일

원국님...! 세상에 마상에 정말 멋지군요..!!

1개의 답글