프론트엔드 프로젝트에 프로토버퍼 적용 고군분투기
1. 초기이슈
- 어떻게 .proto 파일을 임포트 할 것인가
- 어떻게 .proto 파일을 외부에 노출하지 않을 것인가
- 인코딩과 디코딩의 자동화
2. 해결
2-1. 사전준비: 필요 라이브러리
yarn add protobufjs
2-1. 사전준비: 필요 라이브러리
yarn add protobufjs
끝!
2-2. .proto 파일 임포트 이슈
결론부터 말하면 .js 파일로 변환하여 임포트했다. 번거롭게 웹펙이나 번들러 설정을 건드릴 필요도 없고, 외부에 노출되지 않으며 동기적으로 호출할 수 있다.
# .proto 파일들이 ./assets/proto/src 디렉토리에 존재한다고 가정하고,
# 최종 산출물을 ./assets/proto/proto.js 로 익스포트 하는 npm 스크립트
npx pbjs -t json-module -w commonjs -o ./assets/proto/proto.js ./assets/proto/src/*.proto
위 package.json 의 scripts 부분에 삽입한 후 적당한 시점에 실행시켜 준다. 나의 경우에는 dev(start) 와 빌드 전에 실행하도록 했다.
산출된 결과문을 일반적인 모듈형태로 임포트 된다. 참고로 Protobuf.js 의 ProtoRoot 라는 이름의 객체가 반환된다.
import ProtoRoot from '@/assets/proto/proto';
2-3. 인코딩과 디코딩
protobuf.js 공식 사이트에 문서 페이지를 꼼꼼하게 읽어봐도 결국에는 장대한 삽질이 이어졌다. 다음은 인코딩과 디코딩 코드다.
export function encode (data, type) {
const message = ProtoRoot.lookup(type);
const buffer = message.encode(message.fromObject(data)).finish();
return btoa(String.fromCharCode.apply(null, buffer));
}
export function decode (data, type) {
const message = ProtoRoot.lookup(type);
const buffer = Uint8Array.from(atob(data), c => c.charCodeAt(0));
return message.decode(buffer);
}
자, 이제 인코딩, 디코딩용 모듈도 만들었으니 통신용 모듈에 연결하여 response와 request 내용을 가로채서 인코딩 디코딩을 해주면 된다. 보람찬 일이었다. 이제 아무일도 일어나지 않을 거라고 생각했으나...
3. Type: google.protobuf.
3-1. 이제 아무것도 두렵지 않아
복병이 등장하고야 말았던 것이다. 백엔드에서 프로토버퍼를 도입한 후 한가지 이슈가 있었는데 falsy 값을 인코딩하면 어째서인지 그 항목 자체가 사라져 버린다는 것이었다. 그래서 기본 타입말고 구글에서 제공하는 라이브러리를 사용해서 falsy 값이 들어갈 수 있는 항목에 적용을 했는데, 적용후 멀쩡하던 통신들이 에러를 뿜어내기 시작했다.
JSON 데이터의 내부를 살펴보니 금방 원인을 알 수 있었다. 데이터의 구조 자체가 바뀐것이다. 일단 혼란을 막기 위해 위의 타입을 "구글 타입" 이라고 하자.
구글 타입으로 지정된 데이터를 웹에서 받이 디코딩을 해보면 이런 형태가 된다.
// 서버에서 내려준 값
{
foo: 'bar',
}
// 실제로 프론트에서 받은 값
{
foo: { value: 'bar' },
}
기존에 받고 있던 데이터와 형태가 달라졌으니 에러가 발생하는 것. 게다가 어째서인지 iOS와 Android에서는 구글 타입 적용후 해당 빈값이 적용된 항목들이 정상적으로 내려오기 시작했으나 웹에서는 변함없이 내려오는 중이었으니, 웹 프론트 입장에서는 그야말로 개악이었다(일해라 구글... 저거 너네들이 관리하는 라이브러리 아니냐!)
일단, 앱 파트에서 제대로 받아진다고 하니 웹 프론트 파트에서 불편을 감수하기로 했다. 일단, 저 { value } 형태로 내려오는 값들은 원래 그렇게 생겼다고 자기 최면을 걸기로 했고, 내려오지 않는 값들은 디코딩시에 설정된 기본값들을 강제로 주입시켜 주기로 했다.
export function decode (data, type) {
const message = ProtoRoot.lookup(type);
const buffer = Uint8Array.from(atob(data), c => c.charCodeAt(0));
const decoded = message.decode(buffer);
return message.toObject(decoded, { defaults: true });
}
3-2. 인코딩은 어떻게 하지
문제는 인코딩이었다. JSON 데이터를 만들때 구글 타입에 해당하는 값들은 반드시 { value } 형태로 감싸줘야 인코딩이 됐던것이다. 에러가 발생하면 그때마다 수작업으로 해주면 못할것도 없지만 뭔가 스마트하지 않다. 길고긴 프로토버퍼 명세를 보면서 타입을 확인하는 걸 매번 하는 것도 귀찮다.
그냥 하던데로 하고, 인코딩할 때 해당 형태로 파싱하게 하자!
import fp from 'lodash/fp';
// 우리의 친구 Lodash를 사용했지만 로직만 이해한다면 그냥 Javascript 로 짜도 좋고, 다른 라이브러리를 써도 좋다.
const parseMessage = (data, type) => ({
message: ProtoRoot.lookup(type),
parsed: parseData(data, ProtoRoot.lookup(type).fields),
});
const parseData = (data, fields) => {
const convert = (key, value) => (
typeof value === 'object' && key !== 'value'
? parseMessage(value, fields[key].type).parsed
: value
);
return fp.flow(
fp.toPairs,
fp.reduce((acc, [key, value]) => {
acc[key] = convert(key, value);
return acc;
}, {}),
fp.toPairs,
fp.reduce((acc, [key, value]) => {
acc[key] = /^google\.protobuf\./.test(fields[key].type)
? { value: value }
: value;
return acc;
}, {}),
)({...data});
};
export function encode (data, type) {
const { message, parsed } = parseMessage(data, type);
const buffer = message.encode(message.fromObject(parsed)).finish();
return btoa(String.fromCharCode.apply(null, buffer));
};
4. 어쨌든 잘 쓰고는 있습니다
잘 쓰고 있다. 겸사겸사 회사에서 걱정하고 있던 다른 회사에서 API 데이터를 무단으로 긁어가던 문제도 해결됐다(디코딩된 내용을 .proto 파일이 없다면 인간이 알아볼 수 있는 형태로 인코딩 할 수 없다는 점에서는 일종의 암호화일 수도 있다. 물론 암호화와는 근본적으로 다르고, 애초에 이 기술의 개발된 목적과 방향성 자체도 다르다).
근데, 구글님아... 꼭 이렇게 만들어야 했을까? 정말, 이게 최선이었어...?
Author And Source
이 문제에 관하여(프론트엔드 프로젝트에 프로토버퍼 적용 고군분투기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@nekonitrate/프론트엔드-프로젝트에-프로토버퍼-적용-고군분투기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)