Gif processing scenario - (1)

강민수·2023년 1월 20일
0

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개의 댓글