컴퓨터는 아스키코드와 유니코드 이야기때에서도 말했듯, 통신과 저장을 모두 이진수 데이터로 행하게 된다. (바이너리 데이터)
일반적으로 node.js에서 바이너리 데이터의 흐름을 stream이라고 한다.
서버에서 클라이언트에게 데이터를 전송한다면, 크게 두가지의 케이스가 생겨난다
a. 서버가 보내는 속도가 클라이언트가 처리하는거보다 빠름 ( 데이터가 너무 많이 오지 못하도록 조절해야 한다)
b. 서버가 보내는 속도가 클라이언트가 처리하는거보다 느림 ( 데이터가 일정 량 차서 의미있는 실행단위가 될 때까지 기다려야 한다 )
즉, 두가지 케이스를 커버하기 위해 만들어진 것이 buffer이다.
node.js에서 버퍼는, 일정 량만큼의 데이터가 차오를때까지 기다렸다가 일정 단위를 충족하면 그제서야 데이터를 전달하게 된다.
그런데, 내가 마주한 상황은 현재 이와같다.
// pages/api/pdf
export default async (req: NextApiRequest, res: NextApiResponse) => {
try {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000');
await page.emulateMediaType('screen');
const pdfBuffer = await page.pdf({ format: 'A4', printBackground: true }); // 버퍼를 만든 후, pdf를 만들어 데이터를 저장하기를 기다린다.
res.send(pdfBuffer); // pdf만큼의 필요 데이터가 충분하게 차면, 이 파일 바이너리 데이터를 클라이언트에게 전송한다.
await browser.close();
} catch (err) {
alert(err);
}
};
클라이언트 측에서는 위와같이, 함수를 통해 axios를 이용하여 요청을 날리고, 응답을 받아오게 된다. 문제는 이때 받아오는 정보가 내가 예상했던 내용과 사뭇 달랐던 것이다.
이전에 파일 다운로드 처리를 "anchor" 태그를 이용한 download attribute로 처리했을 때에는 파일이 자동으로 다운로드되었는데,
axios로 요청을 날렸을 때에는
이렇게 파일이 다운로드되는 것이 아닌, axios의 응답객체에 전달받아 왔던 것이다.
그래서 이것을 어떻게 꺼내야하는지를 계속 구글링해서 검색해보기 시작했다.
보통 인터넷에서 데이터를 전송하게 될 때, 이메일을 보내기 위해 이용되는 프로토콜인 "SMTP"는 7bit ASCII 로 이루어져있다. (즉, 아까 보았던 1개의 parity bit를 제외한 나머지)
만약 실제 비트가 8비트를 넘어가는 코드를 사용하게 될 경우라면 MIME 타입을 사용한다고 한다.
7bit ASCHII를 제외한, 즉 영어이외의 문자나 그 외 첨부파일등은 8비트 코드 형식이상이 되버리므로 제대로 전송이 되지 않는다. 따라서 MIM타입을 보내주어 해당 데이터가 어떤식으로 파싱되고 읽어들여져야 하는지를 설명해준다.
MIME type에서 대표적인 케이스는 아래와 같다.
1) text
특정 문자셋 혹은 포멧된 텍스트 정보이다.
####ex) text/plain, text/html, text/css, text/javascript
2) image
모든 종류의 이미지를 뜻한다
####ex) image/gif, image/png ...
3) auido
모든 종류의 오디오 ...
그리고 제일 중요한
4) application
모든 종류의 "바이너리 데이터" 를 나타낸다.
즉, 위에서 언급되는 특정할 수 있는 데이터의 타입이 아니라면 기본적으로 바이너리 데이터 처리를 한다.
####ex) application/octet-stream, application/pkcs12, application/vnd.mspowerpoint, application/xhtml+xml, application/xml, application/pdf
아무래도 서버에서 보낼 때 그냥 res.send로 버퍼 데이터를 전송했기 때문에 특정 타입을 추론할 수 없어서 "application/octet-stream" 으로 나왔던 모양이다.
일단 확실한 것은 바이너리 데이터가 전송이 되었다는 것은 알 수 있었다.
그래서, 스텍 오빠플로우를 검색해본결과 이러한 내용을 볼 수 있었기에 한번 시도해보았다.
axios.get("http://my-server:8080/reports/my-sample-report/",
{
responseType: 'arraybuffer',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/pdf'
}
})
근데 컨텐트 타입을 왜 json으로 했는지 설명이 안되있어서 (나는 pdf를 원하고 있으므로) 이것만 pdf로 고쳐서 요청을 날려보았다. 그랬더니
확실히 데이터 타입에 방금전 깨져있던 데이터 타입과는 다르게 명확하게 ArrayBuffer라는 객체가 들어온 것을 볼 수 있었다. (희망이 보인다)
그 후 방법은 좀... 이런건 api로 안만들어주나 싶지만 대체 방법이 존재한다.
현재 환경이 Next.js 즉 서버이긴 하다만 이미 html 이 전송된 상태이므로 클릭 이벤트 핸들러에는 충분히 클라이언트사이트 api인 window 객체의 web api를 사용할 수 있다.
원래는 버튼이 아니라 "anchor" 태그로 했던 내용을, 임시로 노드형태로 만들어서 진행하면 된다.
<Button>
<a href="api/pdf" download={'최우철_프론트엔드.pdf'}>
<AiFillFilePdf />
</a>
</Button> // 즉, 버튼 클릭시 저 a 태그가 하는 작동이 필요한 것이므로
function downloadPDF(buffer: Buffer, filename: string) {
const a = document.createElement('a');
a.href = URL.createObjectURL(
new Blob([buffer], { type: 'application/pdf' }),
);
a.download = filename + '.pdf';
a.click();
URL.revokeObjectURL(blobURL); // 한번 DOM에 blob URL이 등록되었으면, 메모리 누수 방지를 위해 해제하는 편이 좋다. GC 수거해줘
} // 이렇게 함수로 로직을 빼서 만든 후,
const handlePDFRequest = async () => {
try {
const res = await axios.get('api/pdf', {
responseType: 'arraybuffer',
headers: {
'Content-Type': 'application/pdf',
Accept: 'application/pdf',
},
});
downloadPDF(res.data, 'test');
} catch (err) {
console.log(err, '님아 제발 그러지마오...');
}
};
// 클릭 이벤트 핸들러의 최종단에서 처리하게 만들어두었다.
결과는 성공적으로 받아와진 것을 확인할 수 있었다.
근데 여기서 끝내기엔 첨보는 것들이 몇개 있었어서 정리를 좀 하려고 한다
window 객체에서 제공하는 생성자 URL 은 각종 인터넷 주소와 관련된 프로퍼티들을 제공한다
그리고 생성자 함수이므로, 당연히 정적 메서드를 가질 수 있다.
URL 생성자 함수의 정적 메서드에 바로 우리가 궁금해하던 "createObjectURL"이 존재한다
설명이 좀 어렵지만, 다시말하자면 인자로 들어오는 값들 (File, Blob, MediaSource)을 도큐먼트상에 저장하고 이것에 대해 접근할 DOM string을 생성해준다는 의미이다. 이 DOM string이 마치 서버의 엔드포인트와 같은 역할을 한다.
해당 DOM string 주소는 document가 사라지면 같이 사라지기 때문에 윈도우 내에서 남아서 재활용이 되지 않으며,
결과적으로 보안적으로 조금 더 안전하다는 장점이 있다.
일전 유튜브 영상 따보려고 들어갔더니 저렇게 되어있었다. 이 주소를 새로운 주소창에 넣었더니 새로운 탭은 새로운 렌더링 엔진이 관리하는 영역이라서 유튜브 사이트가 켜져있는 랜더링 프로세스와 영역이 달리하므로 접근할 수 없고, 결과적으로 아무것도 나오지 않는다. 정말로 좋은 기능이지 않은가. 이전 저게 뭐지 했던 내용을 이제와서 알게 되다니... 반성한다.
그럼 이제 우리가 전송했던 new Blob 이 무엇인지 알아야 하겠다.
이름 명명 그대로 "겁나 큰 바이너리 데이터 객체" 라는 의미이다.
보통 이미지, 비디오 등의 데이터는 정말 엄청나게 많은 바이트를 가진 데이터이다. 이것을 한번에 보내는 것은 효율적이지 못하므로 분할해서 보낼 필요가 존재하는데 이때 사용하는 것이 Blob 생성자이다.
이 Blob은 송수신을 위한 작은 Blob으로 분할하는 기능 및, 우리가 위에서 봤던 octet-stream과 같이 정확하게 무슨 타입인지 알 수 없는 MIME 타입의 데이터에 대해서도 알아내게 해주는 기능을 가진 고마운 친구다.
const newBlob = new Blob(array, options);
위에 언급한대로, 첫째 인자로는 Blob객체가 컨트롤하기 원하는 바이너리 데이터를 넣어주면 된다. 참고로 Blob 인스턴스 그 자체도 가능하다.
두번째 인자는 옵션객체가 들어가는데, type과 endings 프로퍼티가 설정이 가능하다. type의 경우, 해당 blob이 어떤 MIME타입을 보유할 것인지를 직접 정해주는 것이고, endings는 \n과 같은 이스케이프 문자열의 처리에 대해서 어떻게 할지 설정해주는 값이라고 한다.