pdf viewer 라이브러리 없이 직접 만들기 (feat. aws s3)

김현재·2022년 5월 3일
5

pdf 뷰어를 만들 일이 있었는데 생각보다 어려웠다..!ㅠㅠ
정보도 너무 없고 (영어고 한국어고) 특히나 파일 객체를 네트워크 송수신시 어떻게 가공하고 풀어줘야할지 고민할 부분이 많아서 시행착오를 겪었다.
삽질 기록한다.

문제상황

문제가 총 3단계나 발생했다..

  1. s3 bucket에 올린 pdf파일을 view로 보여주려고 하는데 iframe 상에서 보이지가 않는다
  2. iframe 사용 시 일부 파일 수신에 실패하는 경우, 파일 수신이 200ok가 될 때 까지 계속 req를 보내는 문제가 발생했다. (chrome dev 한정)
  3. 해당 viewer가 있는 페이지에 접속하면, view로 보여줘야 할 pdf파일들이 우르르 다운로드 된다 (내 의지와 상관없이) (chrome 한정)

해결

1. s3 bucket에 올린 pdf 파일이 iframe에 보이지 않는다

왜 보이지 않는가는 네트워크 탭을 통해서 간단히 확인할 수 있었다.
pdf파일이 Response로 들어올 때, Content-Type: application/pdf 여야지만 브라우저가 pdf파일이 들어옴을 인식하고 iframe에서 pdf를 비출 수 있다.
하지만, s3의 경우에는 content-type이 octect-stream으로 기본 설정이 되어있다!
그래서 브라우저가 pdf파일이 들어온 것을 인지하지 못한 것.

또한, Content-Disposition: inline이어야지만 브라우저가 pdf파일이 화면에만 비춰지면 되는 것으로 인식되는데, s3의 경우 기본값이 attachment로..이런 경우에는 파일이 바로 받아지게 된다.
즉, content-type과 content-disposition설정을 변경해주면 된다

1. s3 콘솔에서 메타데이터를 설정한다

가장 확실한 방법으로 s3 콘솔에서 해당 pdf가 저장되는 bucket의 metadata를 Content-Type: application/pdf; Content-Disposition: inline으로 변경해주면 된다.

aws 공식 문서에도 친절하게 설명되어있어, 찬찬히 따라하기만 하면 설정을 변경할 수 있다 (한국어)
https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/add-object-metadata.html

2. s3로 post 시에 content-type을 설정한다

s3로 파일을 전송하는 시점에 헤더부분에 Content-Type: application/pdf; Content-Disposition: inline로 메타데이터를 지정해주면, s3에서 해당 내용으로 파일을 저장하게 된다.
(물론 나는 해봤지만 잘 안되었다;_;)

2. iframe 사용 시 일부 파일 수신에 실패하는 경우, 파일 수신이 200ok가 될 때 까지 계속 req를 보내는 문제가 발생했다. (chrome dev 한정)

왜인지 모르겠지만 chrome, safari, firefox에서는 아무 문제 없었지만 chrome dev의 경우 계속 req가 보내지는 기현상이 일어났다..
아직도 왜 발생하는지는 모르겠지만.. stackoverflow를 뒤져보니..iframe이 웹브라우저 호환성이 별로 좋지 못하다고 하여 <object><embed>로 태그를 바꿔주었다.
그랬더니 언제 그랬냐는 듯이 req가 딱 한번만 보내지고 멈췄다!

// object와 embed사용 예 - object가 호환되지 않는 경우를 대비해 embed도 함께 사용했다
    <object data={base64Url} type='application/pdf' width='100%' height='100%'>
      <embed type='application/pdf' src={base64Url} width='100%' height='100%' />
    </object>

3. 해당 viewer가 있는 페이지에 접속하면, view로 보여줘야 할 pdf파일들이 우르르 다운로드 된다 (내 의지와 상관없이) (chrome 한정)

이 문제가 가장 까다로웠다.
얼마나 chrome이 사용자 친화적인지..네트워크 송수신 시 pdf, jpg, png 등 파일이 res에 담겨오면 반사신경적으로 파일을 다운로드(..!) 해버린다.
알아서 확장자를 읽어서 다운로드 시켜버리는 것이기에, 아무리~~뒤져보아도 다들 크롬 브라우저 내에서 사용자 설정을 변경하는 방법밖에 알려주지 않았다..
심지어 stackoverflow에서조차, 막을 수 없으니 사용자 설정 변경해라라고...말했다.

하지만 역시나 뒤져보다보니 몇년 전 자료를 찾게 되었는데, embed시 src가 pdf확장자를 가지고 있지 않다면..크롬 브라우저가 해당 파일이 다운로드 되어야 하는 파일인지 아닌지 인식을 못한다는 것을 발견했다...!
그리고, pdf확장자를 대체할 수 있는 방법은 바로 base64로 인코딩을 하는 것이였다...!

(이제 어려워진다)
단순히 s3 링크만 가지고 있는 경우, base64로 인코딩 하기 위해선 몇가지 단계를 거쳐야 한다.

  1. 해당 링크를 활용하여 blob 객체를 생성한다
  2. 생성한 blob 객체는 type이 application/pdf"여야 한다
    => pdf여야 embed태그가 pdf파일임을 인지하고 화면에 보이게 할 수 있다
  3. pdf로 타입셋팅된 blob을 base64로 인코딩한다 (제일 쉬움)

아래 코드 해설에 앞서 blob에 대해서 간단하게 설명하자면, (정확하지 않습니다)
blob은 파일 데이터를 가지고 있는 객체(?) 형태의 데이터라고 볼 수 있다...
url, base64, file등 다양한 형태를 blob을 활용하면 동일한 객체의 형태로 만들 수 있고..
그걸 조작해서 내가 원하는 형태 (url, base64, file 등)로 반환할 수 있다.

자세한 내용은 mdn에 너무 잘 나와 있으니..그곳을 통해 보면 좋을 것 같다
https://developer.mozilla.org/en-US/docs/Web/API/Blob

그럼 위의 3단계를 코드로 구현한 것을 리뷰해보자,,

// base64로 인코딩 예시
// pdf viewer는 종종 사용하는 경우가 있으니 이런건 따로 hook처럼(?) 만들어서 사용하면 좋습니다
 const getPdfEncoded = (url) => {
   // url을 그대로 받아와도 되지만, pdf형태로 받아오도록 명시하고 싶어서 fetch로 다시 받아왔다
    fetch(url, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/pdf', // 이래야 브라우저가 pdf라고 인식하고
        'Content-Disposition': 'inline', // 이래야  브라우저가 다운로드말고 화면에 비추도록 한다
      },
    })
      .then((res) => {
        return res.blob() // fetch를 통해 받아온 파일 데이터를 blob객체로 변환하여 전달한다
      })
      .then((blob) => {
      // 받아온 blob 객체를 그대로 넘기면 type이 plain/text로 되기때문에, 
      // 다시 새로운 blob객체를 받아온 것을 참조하여 생성, 이 때 타입을 pdf로 명시한다
      // 이래야지만 base64로 인코딩 될때 pdf형태로 인코딩 된다!
        const newBlob = new Blob([blob], { type: 'application/pdf' }) 
        blobToBase64(newBlob)
      })
  }

 // base64로 인코딩
  const blobToBase64 = (blob: Blob) => {
    const reader = new FileReader() // FileReader는 blob을 읽을 수 있다
    reader.readAsDataURL(blob) // 바이너리 파일을 Base64 Encode 문자열로 반환
    reader.onloadend = () => { // 다 끝난 후 뭐할지 작성
      const base64data = reader.result // base64로 반환된 결과값 저장
      setBase64Url(base64data) // state에 저장해두고 view에 뿌렸다
    }
  }

구현 결과


위의 모든 문제가 해결되었다..^__^
화면에도 잘 나타나고, 혹시나 사이트가 느려질까 걱정했는데 오히려 render를 덜 한다..! (오히려좋아)

참고자료

  • stackoverflow 최고!
profile
쉽게만 살아가면 재미없어 빙고!

3개의 댓글

comment-user-thumbnail
2022년 5월 30일

안녕하세요, 혹시 pdf 파일은 어떻게 생성하여 수신하신건가요?!

1개의 답글