현재 회사의 콘솔 프론트엔드 레포지토리는 모노레포로 되어있고, CI에서
다양한 MSA들의 API가 작성된 JSON Schema를 OpenAPI Generator로 빌드하고, 그 이후 콘솔을 빌드하는 방식으로 되어있다.
OpenAPI Generator가 자바 기반이라 그런지? PNP로 구성되있는 레포임에도 불구하고 불러오고 빌드를 끝마치는데 생각보다 오랜 시간이 들었다.
현재 콘솔의 총 빌드 시간은 10~13분정도 걸린다(이미 NextJS 콘솔과 Jest쪽은 캐싱이 되어있어 그것에 따라 달라짐)
Github CI Checkout 30초
Setup yarn 15초
빌드(콘솔 및 API) 3분 30~ 4분
린트 10초
테스트 3분
버셀 배포 12초
스토리북 배포 2분 30초
캐싱 및 setup node 등 여타 시간 총 포함 30초....
30 + 15 + 240 + 10 + 180 + 12 + 150 하면 637초니까 대충 10~13분 걸리는 것이다.
API쪽 수정이 있어 실질적으로 빌드를 해야하는 경우도 있겠지만, 그것이 아닌 단순 콘솔 코드의 변경이라고 하더라도 CI상으로는 계속 API들을 빌드해야하는 비효율이 생겨 느리다.
따라서 이를 캐싱처리한다.
github에서 제공해주는 actions/cache를 통해 캐싱을 할 수 있다.
- name: Build cache
uses: actions/cache@v3
id: api-cache
with:
path: |
${{ github.workspace }}/library/api/dist
key: ${{ runner.os }}-api-${{ hashFiles('**/library/api/dist') }}
- name: Build api
run: |
yarn workspace api build
if: steps.api-cache.outputs.cache-hit != 'true'
처음엔 캐시 key를 package.json를 해쉬 처리한 것을 통해서 안의 내용이 변경되면(버젼이 변경되거나) 등의 경우에 감지를 할까 했었지만, 이 경우에는, 휴먼 에러로 깜빡하고 API 실 버젼은 올렸는데, Package.json 내용을 변경 안하거나.... 등의 이슈가 생길 가능성이 높기도 하고.... dist를 캐시 키로 삼는게 제일 베스트라 dist를 해시키로 삼았다.
Build cache에서 설정한 id를 통해 cache-hit 여부를 체크할 수 있으며, 이를 통해 빌드를 돌릴지 말지를 결정하게 하였다.
결과적으로 API변경이 없다면(모두 캐시처리하여 빌드를 건너뛴다면)
빌드 시간은 총 45초 정도로 200초 가량 줄일 수 있엇다.
그렇게 어렵지 않게 개선할수 있어서 좋았다.
다시 만들고 있는 철권 사이트 토이 프로젝트에 이제 API를 붙이다가, 클라이언트측에 Firebase를 제거해리고 그냥 Astro쪽 엔드포인트에 Firebase-admin을 붙여서 그쪽으로 통신하면 클라이언트에 파이어베이스를 완전히 떼도 도니까 클라이언트 측이 훨씬 가벼워질거 같아 이쪽으로 방향을 틀었다.
너무 클라이언트에서 모든걸 해결해야한다는 그런 좁은 생각이 들었던거 같다...
또한 AstroJS가 내 상황에 잘 맞는 프레임워크가 맞는가? 라는 생각이 요즘 자꾸 들긴 한다. 뭔가 Island 아키텍쳐로 크게 이득을 보지 못하는 구조로 작성하고 있는거 같은데.... Island Architechture로 이득을 보려면 정적으로 빌드하는 경우....
블로그, 회사 개요 사이트 등이면 의미가 크게 있는거 같은데, 실제로 로그인하고 CRUD하는 사이트에서도 그렇게 이득이 큰지는 잘...?
근데 코드를 보면 NextJS로 금방 마이그레이션 할 수 있는 상황이라 일단 AstroJS로 끝까지 쭉 개발해보고 이후에 NextJS로 덮은거랑 실제로 클라이언트측 번들 사이즈라던지 비교해보면 좋을거 같다.
지금은 서버측에 파이어베이스 API를 엔드포인트로 몰아놓고 React-query도 SSR 되니까 그거 써서 하면 수정삭제추가 이후에 refetch도 자연스럽고 가독성 좋게 구현할수 있을거 같아 NextJS로 짜는게 맞았던거 같기도 하고...? 지금 아스트로 구성에선 Post하고 나서 새로고침 처리해줘야할거 같은데 사용자 경험적으론 안좋은거 같아서...
ReadableStream
Astro 서버 엔드포인트에서 request.body를 받았는데 ReadableStream이 오더라.
JSON로 변환하는건 Response로 한번 감싸고 변환하면 됨. Response에서 인자로 ReadableStream을 받을수가 있기 떄문.
이건 중요한게 아니고 겸사겸사 Stream이 어떻게 작동하는지 공부하자.
ReadableStream
Underlying source는 두개, push source, pull source가 있음.
Push source는 사용자가 한번 액세스후에 계속 푸시하므로, 시작, 일시정지, 취소할지는 사용자한테 달려있음. Video stream이나 TCP/Web sockets가 예시임.
Pull source는 연결한 후에 계속해서 데이터를 요청해야함. fetch/XHR call을 통한 파일 엑세스 과정이 예시임.
data는 chunks라고 부르는 작은 단어로 나누어져서 읽힘. Chunks는 바이트 수준이거나, 특정 크기의 Typed Array일수도 있음. 하나의 스트림은 여러 사이즈와 크기의 청크들을 포함함.
Stream에 위치한 chunks들은 enqueued라고 불림. 읽히기 전까지 queue에 쌓인단 뜻~ Internal queue에서 아직 안 읽힌 청크들을 추적하고 있음.
Stream에 있는 chunks들은 reader라고 불리고, 한번에 하나의 청크를 처리함. Reader나 다른 처리 코드들을 consumer라고 부름.
Controlller: 각 reader들은 사용자가 스트림을 컨트롤할수 있도록 할당된 컨트롤러가 있음.(취소할수 있는 컨트롤러... 등등)
Readable Stream에는 통상적인 readable stream 이외에도 byte stream이라고 있음.
기존의 스트림과 다르게 BYOB(Bring Your Own Buffer) reader로 읽을 수 있음. 이런 reader들은 개발자가 제공된 버퍼를 바로 읽을수 있도록 하여 필요한 복사를 최소화함.
실제로 사용할 스트림은 스트림이 어떻게 생성되는지에 따라 달라짐.
원래는 한번에 하나의 reader만 스트림을 읽을수 있지만, 스트림을 복사하여 두개의 다른 reader로 읽을수 있는데, 이를 teeing이라고 함.
브라우저로 스트리밍하면서 ServiceWorker 캐시에도 스트리밍을 할떄 사용할 수 있음. Response.body가 한번만 소비될수 있고 스트림에 하나의 reader만 읽을수 있으므로 복사본이 필요함.
WritableStream
Raw data가 작성되는 lower-level I/O sink에 대한 추상화를 제공함.
데이터는 writer에 의해 한 chunks씩 기록됨. reader에서의 chunk처럼 여러 형식일수 있음. writer + 관련 코드는 producer로 불림.
Writer가 생성되어 쓰기 시작하면 stream이 잠김. 하나의 writer만 한번씩 stream에 접근할 수 있음. 만약 다른 writer가 작성하길 원하면 중단하고 해야함.
스트림 API는 pipe chain이라는 구조를 통해서 스트림을 다른 스트림과 pipe할 수 있게 해줌.
pipeThrough: TransformStream을 통해서 데이터 형식을 변환함. 비디오 프레임 인코딩/디코딩, 압축/압축해제 등.
TransformStream은 {ReadableStream, WritableStream}으로 구성됨.
pipeTo: 페이프체인의 끝점 역할을 하는 WritableStream에게 파이프를 연결함
pipe chain 시작점을 original source, 끝점을 ultimate sink라고 함.
Backpressure
스트림/파이프 체인에서 읽기/쓰기 속도를 조절하는 과정임. 만약 체인 뒷단에 있는 stream이 아직 새로운 청크를 받아드릴 여유가 없다면, 앞단의 transform stream들에게 속도를 조절하도록 신호를 보내 병목을 발생하지 않도록함.
backpressure를 사용하려면 desiredSize속성을 쿼리하여 consumer가 요구하는 청크사이즈를 컨트롤러에 요청할 수 있음. 나중에 다시 소비자가 데이터를 수신하기를 원할 경우, 스트림 생성에서 풀 방식을 사용하여 기본 소스에 데이터를 공급하도록 지시할 수 있습니다.
Internal queue에 의해서 아직 처리되지 않은 스트림의 청크들이 추적됨.
Readable stream의 경우에는 enqueued되었지만 아직 안 읽힌 청크들이고, writable stream의 경우에는 작성되었지만 아직 처리되지 않은 청크들임. Internal queue에서는 상황에 따라서 backpressure 신호를 보내는 queuing 전략을 사용함.
대체로는 큐가 관리하기 선호하는 가장 큰 크기인 high water mark라는 값과 비교함.
high water mark - total size of chunks in queue = desired size
desired size는 high water mark 크기 이하에서 스트림이 흐름을 유지하기 위해 허용할수 있는 청크 개수임.
청크 생성은 desired size가 0 이상으로 유지하면서 스트림 흐름을 빠르게 유지하기 위해 조절됨. 0 이하로 떨어지면 처리보다 생성이 빨라 문제가 발생할 수 있음.
Readablestream은 locked 속성을 가지고 있으며, cancel, getReader, pipeThrough, pipeTo, tee 메서드를 가지고 있다.
locked 속성은 getReader를 통해 호출되는 reader에 고정되어 있는지 여부를 알려준다. Mutex 느김
cancel : 스트림을 취소되었다는 것을 Resolve하는 Promise를 반환함. 이 메서드 호출한다는거는 Consumer 입장에서 필요없어졌기 떄문. reason 인자가 주어질수도 있음.
getReader: Reader를 생성하고 stream을 거기에다 고정시킴(locked 속성). 고정되면 release되기 전까지는 다른 Reader에서 읽을수 없음.
pipeThrough: 현재 스트림을 TransformStream에 파이핑해주는 기능을 제공함. 파이핑하면 그 동안 스트림이 lock 되므로 다른 스트림이 못 잠금. TransformStream은 {writable,readable}로 된 객체로, 함께 작동해 어떤 데이터를 다른 형식으로 변환하는 기능을 함.
export async function streamToArrayBuffer(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
let result = new Uint8Array(0);
const reader = stream.getReader();
while (true) { // eslint-disable-line no-constant-condition
const { done, value } = await reader.read();
if (done) {
break;
}
const newResult = new Uint8Array(result.length + value.length);
newResult.set(result);
newResult.set(value, result.length);
result = newResult;
}
return result;
}
'TIL' 카테고리의 다른 글
TIL 2023-02-07 Generator (0) | 2023.02.07 |
---|---|
TIL 20233-01-24 Wavesurfer스타일 오디오 재생기 만들기 (0) | 2023.01.24 |
TIL 2022-12-13 Yarn berry + tsc 버그, palindrome dp, Advent of code 2022 day2 (0) | 2022.12.13 |
TIL 2022-12-04 기존 프로젝트 Astro, Unocss로 개편하기 (0) | 2022.12.04 |
TIL 2022-11-29 Unocss, pnpm (0) | 2022.11.29 |