Gif processing scenario - (1)

강민수·2023년 1월 20일

GIF 전송 삽질 일지

GIF 파일을 전송한다는게 쉽지 않은 일이다. gif는 일반 이미지 몇백장정도를 한장으로 만들어 놓았기 때문에, 그만큼 용량이 크다는 뜻이고, 한장만 저장하고 전송해도 용량과 속도문제를 모두 겪을 수 있다!

python PIL을 사용하여 GIF를 생성했을 때 실제로
1장의 이미지 : 100kb -> 200장을 담은 1장의 GIF 46mb
아니 200배만 늘어야 할 것 같은데 400배 가량 이미지 사이즈가 증가해버렸다!

왜그럴까?
gif는 한프레임당 loop, animation등의 additional data가 필요하다. 거기에다 duration 등으로 이미지의 지속 시간까지 증가시킨다면, 그만큼 GIF의 용량의 늘어날 수 밖에 없다.

이번장에서 다룰 내용은 GIF의 크기에 관한 내용이 아니라 backend(fastapi)와 frontend(vue.js) 사이에서 axios로 데이터를 어떻게 전송할 것인가에 대해 알아본다!

base64

제일 간편한 방법이다.
이미지를 byte 형식으로 읽은 후 base64 encoding 및 utf-8 decoding을 진행한다.
-> encoded된 byte type은 json response에 올라가지 않아서 byte에 있는 문자를 그대로 string type으로 옮겨주는 과정을 거친다.(/b abcdefg -> 'abcdefg')
content-type이 중요하다. text로 처리하기 때문에 application/json으로 전달해준다

JSONResponse(content={"image": encoed_img}, media_type="application/json")

이런식으로 json response를 보내주면

 let response = await this.$api('http://127.0.0.1:8000/', 'POST', formData)
 this.returnImg = response['image']

이런식으로 response를 parsing하여 사용하면 된다.
이렇게 나온 이미지를

<v-img :src="`data:image/gif;base64,${returnImg}`" />

단순히 src 태그에 이런식으로 base64 image/gif임을 명시해주고 데이터를 그대로 넣어주면 된다.

하지만 여기서 치명적인 단점이 존재하는데 base64 encoding시 용량이 33%가량 증가한다는 점이다
why?) 기존에 8bit로 표현하던것을 6bit로 표현하면서 2bit씩 남게된다 그렇게 나온 33% 데이터만큼 사이즈가 증가하는 것이다.

그렇다면 사이즈도 33%나 증가하고 인코딩 디코딩하는 수고까지 하면서 이런 방식을 굳이 사용하는 이유가 있을까?
1. 바이너리 데이터를 text형식으로 이용할 수 있다.
-> 이미지를 헤더부분에 넣어서 전송하고 싶을 때, 다른 메타정보와 함께 이미지를 전송하고 싶을 때 사용한다.
2. 안전한 통신(보안적인 이유가 아니다)을 할 수 있다.
->SSL같은 보안장치가 없다면? 보안적인 이유로 사용될 수 있다.(하지만 decoding이 너무 잘 알려져있어서, 소용이 있을 것 같진 않다), 중간 데이터 처리 방식이나 제어 문자등으로 인해 데이터가 손실될 수 있는 문제를 방지해준다고 한다.

Byte

위와 같은 명확한 이유가 없다면 굳이 image처리를 base64로 할 필요는 없는 것 같다! 특히 우리는 gif라는 대용량 이미지를 처리하는 만큼 용량에 민감하고 더욱 효율적인 방법을 찾아야한다.

byte 단위로 데이터를 송신한다면, 보내는 형식 image/gif임을 명시해 주어여한다.
이는 간단하게 fastapi 상에서 fileresponse를 이용하여 명시해줄 수 도있다.

하지만 우리가 할 것은 여기서 한발 더 나아가서 streaming response를 사용할 것이다.
대용량 gif를 보내기 때문에 이 gif를 다 처리할 때까지 서버가 리소스를 잡아먹고 있는것도 client측에서 이 데이터를 기다리다가 한번에 처리하는 것도 비효율적이다. 그렇기 때문에 청크 단위로 데이터를 쪼개서 서버는 한 청크에 대한 처리가 끝나면 데이터를 바로 넘기고 클라이언트는 그 데이터를 받아서 바로바로 처리하는 것이다!

def return_chunk():
    with open(img_path, "rb") as f:  # 비동기 처리
        while True:
            chunk = f.read(1024)
            yield chunk

@app.get("/")
def main():
    return StreamingResponse(return_chunk(), media_type="image/gif")

여기서도 중요한 점은 byte형식이기 때문에 content-type이 image/gif임을 명시해주어야 한다는 것이다. 위 코드처럼 streamingresponse는 generator 단위로 데이터를 보내는것을 알 수 있다. 이렇게 실시간으로 조금씩 데이터를 보내면 front에서는 어떻게 처리해야 할까?

 	const response = await axios.post("http://127.0.0.1:8000/~", params, { headers: { 'Content-Type': 'application/json' }, responseType: 'arraybuffer' }); 
    const chunks = new Uint8Array(response.data);
    let total = chunks.length;
    let chunksArr = new Array();
    let offset = 0;
    for (let i = 0; i < total; i += 1024) {
        chunksArr.push(chunks.slice(offset, offset + 1024));
        offset += 1024;
    }
    const gifBlob = new Blob(chunksArr, { type: 'image/gif' });
    nextImg.value = URL.createObjectURL(gifBlob);

axios에서 streaming으로 데이터를 받을 때 await은 첫 번째 청크에 대해서만 데이터를 기다린다. 그렇기 때문에 첫번째 데이터가 넘어온 후로 계속해서 다음 코드들을 처리할 수 있는 것이다.
blob이라는것은 binary large object의 약자로 다양한 데이터 종류의 대용량 바이너리 데이터를 받을 수 있도록 설계되어있다. gif같은 대용량 데이터를 담기에 적합한 것이다! 이 blob 데이터는 해당 dom(page)를 벗어나면 사라진다. 이 웹 어플리케이션 어딘가에 포인터로서 데이터를 가리키고 있는 blob은 createObjectURL을 통하여 접근할 수 있는 경로를 만들어주어 사용할 수 있게 되는 것이다.

<v-img v-bind:src="currentImg"/>

0개의 댓글