1️⃣ 과제 및 목표
이 기능을 구현하는 과정에서 서버에 코드 보안 취약점 분석을 요청하는데, 예상보다 응답 시간이 오래 걸린다는 문제를 발견했습니다. 기존 시스템에서는 단일 파일 검사에 평균 15~50초가 소요되었으며, 다수 파일을 검사할 경우 순차적으로 검사가 완료된 후에 결과가 제공되어 전체 처리 시간이 지나치게 길어지는 문제가 있었습니다. 예를 들어, 한 파일의 서버 응답 시간이 25.61초일 때, 유사한 파일 4개를 검사하면 약 100초의 시간이 소요되었습니다.
이러한 긴 응답 시간으로 인해 사용자가 페이지를 이탈할 가능성이 높아졌습니다. 특히 "파일 모두 선택" 기능을 통해 다수의 파일을 동시에 검사할 경우, 응답 지연이 누적되면서 사용자 경험이 크게 저하될 수 있다고 판단했습니다. 따라서 검사 속도의 최적화가 필수적이었고, 실시간으로 파일 검사 진행 상황을 제공함으로써 사용자 경험을 개선할 필요가 있었습니다.
이번 과제의 핵심 목표는 다음과 같습니다:
- 검사 속도 개선: 서버 요청에 병렬 처리(parallel processing) 방식을 도입하여 파일 검사 시간을 단축하여 사용자 경험을 개선하는 것.
- 사용자 피드백 제공: 검사 시간이 오래 걸리기 때문에 ProgressBar UI를 구현해 실시간으로 검사 진행 상황을 시각적으로 피드백하는 것.
2️⃣ 문제 정의
위의 목표를 달성하기 위해 다음과 같은 두 가지 주요 문제를 해결해야 했습니다.
1. 상황에 적합한 병렬 처리 방식 도입:
단순히 병렬 처리를 도입하는 것만으로는 충분하지 않았습니다. 각 서버 요청이 독립적으로 처리되는 과정에서 응답 속도는 향상시킬 수 있었지만, 문제는 실시간으로 진행도를 추적하고 이를 UI에 반영할 수 있는 방법이 필요하다는 점이었습니다. 따라서, 병렬 처리 성능 향상과 동시에, 실시간 진행 상태를 추적할 수 있는 방법을 결합한 최적의 병렬 처리 방식을 도입하는 것이 이번 문제 해결의 핵심 과제였습니다.
2. 실시간 ProgressBar UI를 위한 서버 응답 진행도 피드백 부재:
사용자에게 실시간으로 서버 응답 진행도를 표시하려면 서버로부터 관련 피드백 값이 제공되어야 했습니다. 그러나 서버에서는 해당 기능을 지원하지 않았습니다. 실시간 피드백을 구현할 수 있는 여러 대안이 있었으나, 모두 적용이 불가능한 상황이었습니다.
- SSE(Server-Sent Events) :
서버가 클라이언트(브라우저)로 실시간 데이터를 전송할 수 있는 일종의 단방향 통신 방식입니다. 이 방식은 서버에서 진행 상황을 실시간으로 사용자에게 보여주기에 적합하지만, 이를 사용하려면 서버 응답의 Content-Type 헤더에 text/event-stream으로 설정되어 있어야 합니다. 그러나 제가 작업한 서버는 이 설정을 제공하지 않아서 SSE를 사용할 수 없었습니다. - WebSocket :
클라이언트와 서버 간 양방향 통신을 지원하는 프로토콜로, 단일 TCP 연결을 통해 실시간 데이터 송수신이 가능합니다. 그러나 서버 측에서 해당 기능을 제공하지 않았습니다. - 폴링(Polling) :
클라이언트가 주기적으로 서버에 데이터를 요청하여 변경 사항을 확인하는 방식이지만, 이 역시 서버에서 제공되지 않는 기능이었습니다.
멘토님께서는 서버 측에서 WebSocket 및 폴링 같은 실시간 피드백 기능이 제공되지 않는다고 하셨고, 이로 인해 해당 문제를 해결하기 위한 대체 방안이 필요했습니다.
3️⃣ 해결 과정
먼저, 서버 응답 진행도 값을 추적하기에 앞서, 이번 과제의 핵심 목표인 응답 처리 속도 개선 문제를 우선적으로 해결해 보았습니다.
문제 1. 병렬 처리 도입으로 응답 시간 개선
1. 현재 상황에 가장 적합한 병렬처리 함수 탐색
현재 상황을 요약하자면, 각 서버 요청을 병렬로 처리해 응답 속도를 개선하는 동시에, 해당 요청에 대한 진행도를 추적하고 이를 메인 스레드에서 실시간으로 수신하여 UI에 반영할 수 있어야 했습니다.
이에 따라 다양한 병렬 처리 기법을 검토하여, 응답 시간을 줄이고 UI에 실시간으로 반영할 수 있는 가장 적합한 방법을 찾고자 했습니다.
Promise
자바스크립트에서 비동기 작업을 처리할 때 자주 사용되는 Promise는 다양한 패턴으로 병렬 처리를 지원하지만, 각 방식마다 진행 상황을 실시간으로 추적하고 UI를 업데이트하는 데 적합하지 않을 수 있습니다. 아래는 각 Promise 메서드의 특징과 한계입니다.
- Promise.all :
여러 개의 비동기 작업(Promise)을 병렬로 실행하고, 모든 작업이 성공적으로 완료될 때까지 기다린 후 결과를 반환합니다. 하나라도 실패하면 전체가 실패로 간주됩니다. 그러나, 중간에 진행 상황을 실시간으로 추적할 수 없고, 각 작업이 완료될 때마다 UI를 업데이트하는 것이 어렵습니다. 하나라도 실패하면 전체가 실패로 처리됩니다. - Promise.allSettled :
모든 Promise가 완료될 때 까지 기다린 후, 성공 여부와 관계없이 각 작업의 결과를 반환합니다. 실패한 작업도 포함해서 모든 결과를 배열로 반환합니다. 그러나, Promise.all과 마찬가지로 중간 상태를 추적하거나 UI를 실시간으로 업데이트하는 데 적합하지 않습니다. 모든 작업이 완료된 후에만 결과를 반환하기 때문에 실시간 피드백을 제공하기 어렵습니다 - Promise.rece :
여러 Promise 중 가장 먼저 완료된 하나의 작업만 반환하고, 나머지는 무시합니다. 성공이나 실패에 관계없이 가장 먼저 응답한 작업의 결과를 반환합니다. 그러나, 병렬 작업을 모두 처리할 수 없으며, 첫 번째로 완료된 작업만 반환되기 때문에, 여러 비동기 작업의 진행 상황을 실시간으로 관리하는 데 적합하지 않습니다 - Promise.any :
여러개의 Promise 중 가장 먼저 성공한 작업의 결과를 반환합니다. 실패한 작업은 무시되고, 최소 하나의 성공한 작업만 반환됩니다. 만약 모든 작업이 실패한 경우에만 에러가 발생합니다. 그러나, Promise.race와 유사하게 모든 작업을 처리하지 않으며, 부분적으로 성공한 작업만 처리하기 때문에 전체적인 진행 상황을 관리하기에는 부적합합니다.
브라우저 환경에서 병렬 처리를 위한 스레드를 사용할 수 있는 여러 방식이 있지만, 각각의 용도가 다르고, UI와 실시간 상호작용하는 작업에 적합하지 않은 경우가 많습니다
- Service Workers :
주로 네트워크 요청을 캐싱하거나 백그라운드에서 서버와 통신하는 데 사용됩니다. 특정 네트워크 작업에 특화되어 있고, UI와 직접적인 상호작용이 제한적입니다. 그러나, 주로 네트워크 관련 작업에 특화되어 있으며, 실시간으로 UI와 상호작용하는 작업에는 적합하지 않습니다. - Worklets (CSS Pain API, AudioWorklet) :
웹에서 작은 작업을 백그라운드에서 처리할 수 있게 하는 경량 스레드입니다. 주로 CSS 페인팅, 오디오 처링, 타이포그래피와 같은 고성능 작업을 처리합니다. 그러나, CSS 렌더링이나 오디오 처리같은 매우 제한적인 영역에 최적화 되어있습니다. 따라서 서버 요청과 같은 네트워크 작업을 처리하고, 그 진행도를 추적하며 UI에 실시간으로 반영하는 데에는 적합하지 않습니다 - Shared Workers :
여러 브라우저 탭 또는 여러 스크립트 간에 공유되는 Web Worker의 일종입니다. 하나의 Shared Worker는 동일한 출처(Origin) 내의 모든 페이지에서 사용할 수 있으며, 여러 페이지에서 공유된 데이터나 리소스에 접근할 수 있습니다. 그러나, 여러 탭에서 데이터를 공유해야할 때만 유용하지만, 단일 페이지 내에서 서버 요청을 처리하고 실시간으로 UI를 업데이트하는 용도에는 과도한 선택일 수 있습니다.
Web Worker
다양한 병렬 처리 기법을 비교한 결과, Web Worker가 가장 적합한 방법임을 알게 되었습니다.
메인 스레드와 독립적으로 백그라운드에서 작업을 처리하며, 복잡한 연산이나 서버 요청을 메인 스레드를 차단하지 않고 병렬로 처리할 수 있습니다. 또한, 메시지 기반의 통신을 통해 작업 진행 상황을 실시간으로 추적하고, 이를 메인스레드에서 UI에 반영할 수 있는 구조를 지원합니다.
- 복잡한 서버 요청을 백그라운드에서 처리함으로써 메인 스레드의 성능을 유지할 수 있습니다.
- 메인 스레드와의 메시지 통신을 통해 작업 진행도를 실시간으로 UI에 반영할 수 있어, 사용자 경험을 향상시킵니다.
이러한 이유로 Web Worker는 병렬 처리를 통해 응답 시간을 개선하고, UI 업데이트를 실시간으로 관리하는 데 가장 적합한 선택이었습니다.
2. Web Worker 활용한 ProgressBar UI 반영
💡 2-1. 검사하기 버튼 클릭 시 선택한 파일 배열 크기만큼 워커 생성하기
모달 창에서 "검사하기" 버튼을 클릭하면, 선택된 파일 배열의 크기만큼 Web Worker를 생성합니다. 이때, 각 파일의 검사 작업은 별도의 워커에서 병렬로 처리되며, 모든 파일 검사가 완료되어야만 Firebase에 저장할 수 있습니다.
워커들이 처리되는 동안 각 워커의 진행 상태를 추적하기 위해, 모든 워커를 Promise로 감싸고, 각 워커가 검사를 완료하면 resolve()를 호출합니다. 이후 Promise.all()을 사용하여 모든 워커의 작업이 완료된 후에 Firebase 저장 작업이 실행됩니다.
- worker.postMessage: 메인 스레드에서 워커로 메시지를 보내 파일 검사를 시작합니다.
- worker.onmessage: 워커가 작업 진행도를 메인 스레드로 전달할 때 실행되는 이벤트 핸들러입니다.
// AnalysisModal.tsx
/**
* 검사하기 누를 경우 실행되는 로직
* web worker가 실행됨.
*/
const getAnalyzeFiles = async () => {
// 검사하기 누를 경우
const workerPromises = selectedFiles.map((file) => {
return new Promise<void>((resolve, reject) => {
const worker = new Worker(
new URL(`../../../worker/analyzeWorker.ts`, import.meta.url),
);
addWorker(worker); // 워커 스토어에 워커 추가
worker.postMessage({
fileId: file.sha,
name: file.name,
content: file.content,
apiUrl: `/api/analyze/llm`,
currState: currentStep,
});
worker.onmessage = (event) => {
const { fileId, name, content, percent, result, status, type } =
event.data;
// 현재 단계
if (type === "progress") {
// 검사중
setCurrentStep("analyze");
setReposState({ repoId: "", repoName: repo.id, state: "analyze" }); // 레포 검사 상태
setAnalyzeFiles({ fileId, progressValue: percent, state: status }); // 검사중인 파일
} else if (type === "completed") {
// 개별 검사 완료
setResultData({ sha: fileId, name, content, result }); // 검사 이력
worker.terminate(); // 워커 종료
resolve(); // 워커 작업 완료 시 resolve 호출
} else if (type === "error") {
console.error(
`Error processing file ${fileId}: ${event.data.message}`,
);
worker.terminate();
reject(event.data.message); // 에러 발생 시 reject 호출
selectedFiles.forEach((file) => {
setAnalyzeFiles({
fileId: file.sha,
progressValue: 0,
state: "canceled", // 상태를 "canceled"로 설정
});
});
setCurrentStep("cancel"); // 상태를 cancle로 변경하여 중단 시킴
}
};
});
});
/**
* 검사 중단 함수
*/
const handleCancel = () => {
clearWorkers(); // 모든 워커 종료
selectedFiles.forEach((file) => {
setAnalyzeFiles({
fileId: file.sha,
progressValue: 0,
state: "canceled", // 상태를 "canceled"로 설정
});
});
setCurrentStep("cancel"); // 상태를 cancle로 변경하여 중단 시킴
};
💡 2-2. 워커와 메인스레드 간 통신하기
메인스레드는 워커로부터 메시지를 받아 각 파일의 진행 상태를 업데이트합니다.
- updateProgress() :
워커의 진행 상태를 1초 간격으로 추적하여 setInterval() 함수를 사용해 현재 진행 상태를 업데이트합니다. 이 상태는 updateProgress()를 통해 메인스레드로 전달되며, 메인스레드는 이를 기반으로 UI를 갱신합니다.
이와 같은 방법으로 서버 응답 진행도를 계산한 이유는 아래 부분에 자세하게 적혀있습니다.
// src/worker/analyzeWokrer.ts
self.onmessage = async (event) => {
const { fileId, name, content, apiUrl, currState } = event.data;
try {
// 진행 상황 업데이트를 위한 함수
const updateProgress = (percent: number, status: string) => {
self.postMessage({
fileId,
name,
content,
percent,
status,
type: "progress",
});
};
// "cancel" 상태일 경우 워커를 바로 종료
if (currState === "cancel") {
updateProgress(0, "canceled");
self.postMessage({
fileId,
name,
content,
percent: 0,
status: "canceled",
type: "canceled",
});
self.close(); // 워커 종료
return; // 함수 종료
}
// 총 요청 시간 (60초)
const updateInterval = 1000; // 1초 간격으로 진행 상황 업데이트
const progressSteps = [
{ percent: 20, duration: 10000 }, // 10초 동안 20%
{ percent: 30, duration: 10000 }, // 다음 10초 동안 30%
{ percent: 50, duration: 10000 }, // 다음 10초 동안 50%
{ percent: 70, duration: 10000 }, // 다음 10초 동안 70%
{ percent: 80, duration: 10000 }, // 다음 10초 동안 80%
{ percent: 100, duration: 10000 }, // 나머지 10초 동안 100%
];
let currentStep = 0;
let previousPercent = 0;
let elapsedTime = 0;
// 각 단계에서 목표 진행률까지 조금씩 증가시키며 업데이트
const updateIntervalId = setInterval(() => {
if (currentStep < progressSteps.length) {
const step = progressSteps[currentStep];
const stepDuration = step.duration;
const stepPercentIncrement =
(step.percent - previousPercent) / (stepDuration / updateInterval);
elapsedTime += updateInterval;
const percent = Math.min(
previousPercent +
stepPercentIncrement * (elapsedTime / updateInterval),
step.percent,
);
updateProgress(Math.round(percent), "progress");
if (elapsedTime >= stepDuration) {
previousPercent = step.percent;
elapsedTime = 0;
currentStep++;
}
}
}, updateInterval);
// POST 요청을 비동기로 수행
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Cache-Control": "no-store",
},
body: JSON.stringify({ content }),
});
if (!response.ok) {
throw new Error(`Failed to check file ${fileId}: ${response.statusText}`);
}
// 요청 결과
const result = await response.text();
// 진행 상황 업데이트 정지
clearInterval(updateIntervalId);
// 완료시 메인 스레드로 보냄
updateProgress(100, "completed");
self.postMessage({
fileId,
name,
content,
percent: 100,
result,
status: "completed",
type: "completed",
});
} catch (error: any) {
self.postMessage({
fileId,
name,
content,
percent: 0,
status: "error",
message: error.message,
type: "error",
});
}
};
💡 2-3. 메인스레드에서 받은 값을 ProgressBar UI에 반영하기
메인스레드는 각 워커에서 받은 파일의 진행도 정보를 기반으로 UI를 업데이트합니다.
- ProgressBar UI 업데이트:
메인 스레드는 워커들이 처리 중인 파일의 상태와 진행도를 추적하여 ProgressBar UI에 실시간으로 반영합니다. 파일이 검사 중인 경우, 모달 창의 상태를 "analyze"으로 변경하고, 워커가 보낸 메시지를 기반으로 파일의 진행도 값을 업데이트합니다.
- 검사 완료 및 워커 종료:
워커가 검사를 완료하고 서버 통신이 성공적으로 마무리되면, 해당 파일의 상태를 "completed"로 변경하고, 워커는 종료됩니다. 이때, resolve() 함수를 호출하여 해당 워커가 성공적으로 작업을 완료했음을 알립니다. - 에러 처리:
검사 도중 에러가 발생할 경우, reject() 함수를 호출하여 오류 상태를 처리합니다. 이때, 모든 파일의 진행 상태는 0으로 초기화되며, 모달 창의 상태는 "cancel"으로 변경됩니다.
// AnalysisModal.tsx
const getAnalyzeFiles = async () => {
// 검사하기 누를 경우
const workerPromises = selectedFiles.map((file) => {
return new Promise<void>((resolve, reject) => {
const worker = new Worker(
new URL(`../../../worker/analyzeWorker.ts`, import.meta.url),
);
addWorker(worker); // 워커 스토어에 워커 추가
worker.postMessage({
fileId: file.sha,
name: file.name,
content: file.content,
apiUrl: `/api/analyze/llm`,
currState: currentStep,
});
// 워커에서 메시지 수신하기
worker.onmessage = (event) => {
const { fileId, name, content, percent, result, status, type } =
event.data;
// 현재 단계
if (type === "progress") {
// 검사중
setCurrentStep("analyze");
setReposState({ repoId: "", repoName: repo.id, state: "analyze" }); // 레포 검사 상태
setAnalyzeFiles({ fileId, progressValue: percent, state: status }); // 검사중인 파일
} else if (type === "completed") {
// 개별 검사 완료
setResultData({ sha: fileId, name, content, result }); // 검사 이력
worker.terminate(); // 워커 종료
resolve(); // 워커 작업 완료 시 resolve 호출
} else if (type === "error") {
console.error(
`Error processing file ${fileId}: ${event.data.message}`,
);
worker.terminate();
reject(event.data.message); // 에러 발생 시 reject 호출
selectedFiles.forEach((file) => {
setAnalyzeFiles({
fileId: file.sha,
progressValue: 0,
state: "canceled", // 상태를 "canceled"로 설정
});
});
setCurrentStep("cancel"); // 상태를 cancle로 변경하여 중단 시킴
}
};
});
});
💡 2-4. 모든 처리가 완료되면 파일 검사 이력 Firebase에 저장하기
Web Worker들의 서버 통신 작업이 모두 완료되면, Promise.all()을 통해 저장 작업을 시작합니다. 이때 각 워커가 정상적으로 작업을 완료한 후, 다음 단계에서 Firebase에 검사 이력을 저장합니다.
- 검사 단계 "finish"로 변경
모든 파일의 검사가 완료되면, 모달 창의 검사 단계를 "finish"로 변경하여 UI 상에서 검사가 성공적으로 종료되었음을 나타냅니다. 이후 저장 작업이 시작되며, 이 단계에서 검사 완료 시간을 기록합니다. - 저장 완료 시간 기록
검사 완료 시간을 기록하고, Firebase에 데이터를 저장할 때 이 시간을 구분자로 사용하여 검사 이력을 관리합니다. 각 저장 작업이 완료되면, 완료된 시간을 초 단위까지 포함한 문자열로 인코딩하여 저장합니다. 이 시간값을 사용하여 나중에 검사 결과 페이지에서 해당 검사 기록을 불러올 때, 정확한 데이터를 패칭 할 수 있게 처리합니다. - Firebase 사용량 최적화
Firebase의 저장 용량이 제한되어 있어, 모든 검사 이력을 저장하지 않고 마지막 검사 이력만을 저장하도록 설계했습니다. 마지막 검사 이력을 구분하기 위해, 저장 시 검사 완료 시간을 포함한 문자열을 생성하여 고유 식별자로 활용합니다. 이렇게 저장된 데이터를 통해 나중에 검사 결과 페이지에서 정확한 검사 이력을 불러올 수 있습니다.
try {
// 모든 워커 작업이 완료될 때까지 대기
await Promise.all(workerPromises);
// 모든 작업이 끝나면 finish로 변경
setCurrentStep("finish");
// 특정 형식으로 날짜 data 문자열 format
const today = format(new Date(), "yyyy-MM-dd HH:mm:ss");
// base64 encoding
const encodingSaveTime = btoa(today);
setSaveTime(encodingSaveTime);
// 레포 검사 상태
setReposState({
repoId: encodingSaveTime,
repoName: repo.id,
state: "finish",
});
// 레포 검사 결과
setAnalyzeFileResult({
repoId: encodingSaveTime,
repoName: repo.id,
data: [...resultData],
});
if (!isOpenModal) {
resetAnalyze();
}
} catch (error) {
console.error("One or more workers failed:", error);
}
};
문제 2. 서버 응답 시간을 기반으로 Progress UI 구현하기
위에서, 병렬 처리를 통해 워커가 진행 상황을 추적하고 이를 메인 스레드로 메시지를 전송하여 UI를 업데이트하는 방식으로 응답 속도를 문제를 해결하였습니다. 이제, 서버 응답 진행도를 어떻게 나타낼지에 대한 문제를 해결해 나갔습니다.
가설 1. Chunked 응답을 기반으로 실시간 진행도를 추적할 수 있다
서버 응답 헤더에서 transfer-encoding: chunked가 설정되어 있는 것을 확인했습니다. 이는 서버가 데이터를 청크 단위로 나누어 전송하는 방식으로, 전체 데이터를 한 번에 보내지 않고 여러 작은 부분으로 나눠 전송합니다. 따라서 서버에서 전송하는 청크 단위와 최대 응답 크기를 계산하면, 이를 기반으로 진행도를 실시간으로 나타낼 수 있을 것이라 판단했습니다.
1-1. 서버 응답 청크 확인
우선, 서버에서 실제로 데이터를 청크 단위로 보내는지를 확인하기 위해 터미널에서 다음 명령어를 사용했습니다:
- curl --trace -
- curl: 명령어 기반 HTTP 클라이언트로, 서버와 HTTP 요청을 주고받는 데 사용됩니다.
- --trace -: 요청과 응답의 세부 정보를 출력합니다. 이 옵션을 사용하면 요청 및 응답의 모든 세부사항(헤더, 상태 코드 등)이 콘솔에 출력됩니다. -는 출력을 표준 출력(stdout)으로 보낸다는 뜻입니다
- . -X POST:
이 옵션은 HTTP POST 요청을 명시합니다. 데이터를 서버에 전송하기 위해 사용됩니다.
curl --trace - -X POST http://(서버주소):8000/generate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzZmFjc3BhY2VfNyIsImV4cCI6MTcyNzg(토큰정보)" \
-d @data.json
// data.json
{
"user_message": "import { useState, useEffect } from 'react';\\nimport menuDot from '/public/images/menu-dot.png';\\nimport Image from 'next/image';\\nimport plus from '/public/images/plus.png';\\nimport LibrarySort from './LibrarySort';\\n\\nexport default function ClippingArticleItem() {\\n let array = new Array(15);\\n let arrLength = Math.ceil((array.length * 170) / 3);\\n const [styleHeight, setStyleHeight] = useState<number | string>(600);\\n const [styleOverflow, setStyleOverflow] = useState('hidden');\\n const [count, setCount] = useState<number | string>(arrLength);\\n const [showMore, setShowMore] = useState(false);\\n const [miniModal, setMiniModal] = useState<number | null>(null);\\n\\n useEffect(() => {\\n if (12 < array.length) {\\n setShowMore(!showMore);\\n }\\n }, []);\\n\\n useEffect(() => {\\n if (styleHeight < count) {\\n setStyleOverflow('hidden');\\n } else {\\n setStyleOverflow('visible');\\n }\\n }, [moreHandler]);\\n\\n function moreHandler() {\\n if (styleHeight < count) {\\n setStyleHeight((prev) => (typeof prev === 'number' ? prev + 600 : prev));\\n } else {\\n setStyleHeight('auto');\\n }\\n }\\n\\n function miniModalHandler(index: number) {\\n setMiniModal((prev) => (prev === index ? null : index));\\n }\\n\\n return (\\n <>\\n <LibrarySort />\\n <div\\n style={{\\n height: styleHeight < count ? `${styleHeight}px` : 'auto',\\n overflow: styleOverflow,\\n }}\\n className='grid grid-cols-4'\\n >\\n {array\\n .fill({\\n label: '취약성 보고서',\\n imgSrc: menuDot,\\n title: 'Microsoft의 여러 보안 취약점에 대한 CNNVD의 보고서',\\n sub: '2024.03.08 13:30:24',\\n })\\n .map((item, index) => (\\n <div\\n key={index}\\n className='mt-[30px] flex h-[170px] w-[310px] flex-col justify-between rounded-xl border border-solid border-[#C3C3C3] px-4 py-4'\\n >\\n <div className='relative flex h-[30px] justify-between'>\\n <div\\n className={`h-[23px] w-[83px] rounded-full px-1 py-[2px] text-center text-[12px] font-semibold ${item.label === '취약성 알림' ? 'bg-[#F2EBFF] text-[#6100FF]' : item.label === '취약성 보고서' ? 'bg-[#F1F1F1] text-[#969696]' : 'bg-[#FFEFEF] text-[#FF6D6D]'}`}\\n >\\n {item.label}\\n </div>\\n <div\\n className='w-[10px] cursor-pointer'\\n onClick={() => miniModalHandler(index)}\\n >\\n <Image src={item.imgSrc} alt='삼단바' width={3} height={17} />\\n </div>\\n {miniModal === index && (\\n <div className='absolute right-[-1px] top-5 flex h-[94px] w-[70px] flex-col justify-center gap-4 rounded-lg bg-[#ffffff] text-[16px] font-medium shadow-md'>\\n <span className='w-full text-center'>삭제</span>\\n <span className='w-full text-center'>공유</span>\\n </div>\\n )}\\n </div>\\n <div className='flex flex-col gap-2'>\\n <span className='text-[18px]'>{item.title}</span>\\n <span className='text-[14px] font-light text-[#969696]'>{item.sub}</span>\\n </div>\\n </div>\\n ))}\\n </div>\\n {styleOverflow === 'hidden' && (\\n <div className='mt-10 flex w-full justify-center'>\\n <button\\n className='flex h-[54px] w-[103px] items-center justify-center rounded-lg border border-solid border-[#6100FF] text-[#6100FF]'\\n onClick={moreHandler}\\n >\\n <span>more </span>\\n <div>\\n <Image src={plus} alt='더보기' width={18} height={18} />\\n </div>\\n </button>\\n </div>\\n )}\\n </>\\n );\\n}\\n\\nquestion:Please translate the security vulnerability analysis result of the above code into Korean and respond only in Korean according to the requested format. Save the object arrangement below for the security vulnerability analysis result of each code. securityRes:[{ title: string; // code vulnerability, description: string; // code vulnerability description ,code: string; // code vulnerability code, line: number; // code vulnerability code line }] Once the security vulnerability analysis is complete, create a sprint-based calibration proposal and store it in the object array below. suggestRes:[{ title: string; // calibration proposal, description: string; // calibration description, code: string; // calibration code, line: number; // calibration code line }] title, description should be in Korean and only securityRes and sugestRes should respond. Please answer all quotes using only large quotes instead of small ones",
"temperature": 0.6,
"top_p": 0.9
}
1-2. 진행도 계산을 위한 청크 수 추정
진행도를 실시간으로 표시하기 위해, ReadableStream을 사용하여 청크가 수신될 때마다 데이터를 콘솔에 출력했습니다.
처음 시도했을 때는 각 청크를 받을 때마다 처리하려 했으나, 모든 청크가 마지막에 한꺼번에 출력되었습니다. 터미널에 찍힌 내용을 보면 222와 333 사이에 받아오는 청크가 생길 때마다 터미널에 찍히게 코드를 작성했었는데 모든 청크를 다 받아온 후 모든 청크 내용이 마지막에 출력되었습니다.
추가적으로 콘솔에 수신한 바이트 수와 청크 수를 출력한 결과, 하나의 청크 안에 모든 데이터를 담아서 응답하는 것을 확인했습니다. 예를 들어, 청크가 1개로 수신되고, 총 2314바이트의 데이터가 한 번에 전송되는 상황이었습니다.
1-3. 청크 응답 테스트 결과 분석 및 문제점 발견
위 결과를 바탕으로, 청크 단위로 데이터를 나누어 전송하지 않는 이유를 조사했습니다. 구글링을 통해 확인한 바에 따르면, 서버가 보내려는 청크 사이즈가 너무 크게 설정된 경우, 서버는 데이터를 작은 청크로 나누지 않고, 지정된 크기만큼 데이터를 모아서 한 번에 전송할 수 있습니다. 이는 데이터 전송 효율을 높이기 위한 설정이지만, 이번 경우처럼 실시간으로 진행도를 추적해야 할 때는 문제가 될 수 있습니다.
특히 이번 상황에서는 서버에 수정을 요청할 수 없었기 때문에 청크를 활용한 실시간 진행도 추적이 불가능하였고 다른 방법을 찾아야 한다고 결론을 내렸습니다.
가설 2 : 서버 응답 시간의 평균을 기반으로 진행도를 나타낼 수 있다.
서버에서 정확한 청크 단위의 응답을 실시간으로 받아 진행도를 표시하는 것이 어려운 상황이었지만, 서버 응답 시간의 평균을 기반으로 진행도를 계산하면 일정 수준의 신뢰성 있는 값을 제공할 수 있을 것이라고 판단했습니다. 따라서, 서버 응답 시간을 추정하여 진행률을 표시하는 방법을 적용했습니다.
2-1. 평균 서버 응답 시간 계산
서버 응답 시간은 15초에서 50초 정도의 범위로 측정되었습니다. 이 범위를 바탕으로 평균 응답 시간을 계산한 후, 진행률의 최댓값을 설정하는 방식으로 진행도를 구현했습니다.
- 평균 응답 시간 범위: 15초 ~ 50초
- 진행률의 최댓값: 응답 시간이 최소 15초, 최대 50초이므로, 그 범위 내에서 진행도를 점진적으로 증가시키도록 설계했습니다.
2-2. 진행도 증가 방식
응답이 완료될 때까지의 진행도를 1초 단위로 조금씩 증가시키는 방식으로 처리했습니다. 이를 통해 사용자에게 응답이 지연되는 동안에도 시스템이 동작하고 있다는 피드백을 제공할 수 있었습니다. 구체적인 진행 방식은 다음과 같습니다:
- 진행률 설정:
서버 응답 시간에 따라 진행률을 단계적으로 설정했습니다. 평균 응답 시간이 15초에서 50초 사이로 예상되므로, 이를 기반으로 10초 단위로 진행률을 나누어 설정했습니다. 예를 들어:- 첫 10초 동안 진행률은 20%까지 증가합니다.
- 그다음 10초 동안은 30%까지 증가합니다.
- 다음 10초 동안은 20% 더 증가하여 50%에 도달합니다.
- 진행도 업데이트:
진행도는 1초 간격으로 갱신되며 증가합니다. 진행률이 응답 시간 범위(최대 50초)에 맞춰 적절히 올라가도록 하여, 서버 응답이 완료되기 전까지 사용자에게 지속적인 피드백을 제공합니다. - 응답 완료 시 진행률 100%로 설정:
서버 응답이 완료되면, 현재 진행률이 얼마이든 상관없이 즉시 100% 로 설정합니다. 이로써 사용자는 서버 응답이 성공적으로 완료되었음을 직관적으로 알 수 있습니다. 응답 시간이 예상보다 짧더라도, 진행률은 응답이 완료되는 즉시 100%에 도달하도록 하여 정확한 피드백을 제공합니다.
// 총 요청 시간 (60초)
const updateInterval = 1000; // 1초 간격으로 진행 상황 업데이트
// 각 단계별 목표 진행률(percent)와 그에 도달하기까지의 시간(duration)을 정의한 배열
const progressSteps = [
{ percent: 20, duration: 10000 }, // 첫 10초 동안 20% 진행
{ percent: 30, duration: 10000 }, // 다음 10초 동안 30% 진행
{ percent: 50, duration: 10000 }, // 다음 10초 동안 50% 진행
{ percent: 70, duration: 10000 }, // 다음 10초 동안 70% 진행
{ percent: 80, duration: 10000 }, // 다음 10초 동안 80% 진행
{ percent: 100, duration: 10000 }, // 마지막 10초 동안 100% 진행
];
let currentStep = 0; // 현재 단계 인덱스 (처음엔 0부터 시작)
let previousPercent = 0; // 이전 단계에서의 완료된 퍼센트 값
let elapsedTime = 0; // 현재 단계에서 경과된 시간 (매 단계마다 초기화)
// 각 단계에서 목표 진행률까지 조금씩 증가시키며 진행률을 업데이트하는 함수
const updateIntervalId = setInterval(() => {
// 진행 단계가 남아 있을 때만 실행
if (currentStep < progressSteps.length) {
const step = progressSteps[currentStep]; // 현재 단계 정보
const stepDuration = step.duration; // 현재 단계의 총 지속 시간
// 매 1초마다 증가해야 할 퍼센트 계산
const stepPercentIncrement =
(step.percent - previousPercent) / (stepDuration / updateInterval);
elapsedTime += updateInterval; // 경과 시간에 1초를 더함
// 현재까지의 퍼센트를 계산 (초과하지 않도록 최소값으로 설정)
const percent = Math.min(
previousPercent + stepPercentIncrement * (elapsedTime / updateInterval),
step.percent, // 현재 단계의 목표 퍼센트를 넘지 않도록 제한
);
// 계산된 퍼센트를 업데이트하는 함수 호출 (UI에 반영될 수 있음)
updateProgress(Math.round(percent), "progress");
// 현재 단계가 완료되었는지 확인
if (elapsedTime >= stepDuration) {
previousPercent = step.percent; // 이전 퍼센트를 현재 단계의 목표 퍼센트로 업데이트
elapsedTime = 0; // 경과 시간 초기화 (다음 단계로 넘어가기 위해)
currentStep++; // 다음 단계로 이동
}
}
}, updateInterval); // 1초(1000ms)마다 이 함수가 실행됨
4️⃣ 성과 및 결과
응답 시간 개선
- 이전 다수 파일 검사 응답 시간:
- 개별 파일 검사 시간: 25.61초
- 4개의 파일 처리 시 총 소요 시간: 약 100초 (25.61초 * 4)
- 개선된 다수 파일 검사 응답 시간:
- 병렬 처리 도입 후 4개 파일 처리 소요 시간: 32.14초
병렬 처리 도입 전에는 각 파일을 순차적으로 검사하였기 때문에 4개의 파일을 처리하는 데 약 100초가 소요되었습니다. 그러나 병렬 처리를 도입한 후, 처리 시간은 32.14초로 단축되었습니다. 이는 약 68%의 성능 개선을 이룬 것으로, 처리 속도가 약 3.1배 향상됐음을 의미합니다.
사용자 경험 향상:
사용자 경험 측면에서, ProgressBar UI의 도입으로 사용자는 실시간으로 검사 진행 상황을 시각적으로 확인할 수 있게 되었습니다. 이로 인해 사용자는 시스템이 동작 중이라는 것을 명확히 인식하게 되었으며, 대기 시간 동안의 불안감이나 불만을 줄이는 데 성공했습니다.
- 시각적 피드백 제공: 진행 상황을 실시간으로 반영함으로써, 파일 검사가 완료될 때까지 사용자가 예측 가능할 수 있게 하였습니다.
5️⃣ 배운 점
1. 병렬 처리의 중요성
이번 프로젝트를 통해 비동기 처리와 병렬 처리가 시스템 성능에 미치는 막대한 영향을 체감했습니다. 특히, 다수의 파일을 순차적으로 처리하는 기존 방식에 비해 병렬 처리가 서비스 성능을 극적으로 향상시킨다는 사실을 알게 되었습니다. 이를 통해 리소스 효율성을 극대화하고 처리 속도를 높이는 방법을 깊이 이해할 수 있었으며, 복잡한 작업을 처리할 때 효율적인 방법을 설계하는 능력을 깊게 고민해 볼 수 있게 되었습니다.
- 효율성 극대화: 시스템이 처리할 수 있는 병렬성을 극대화함으로써, 자원을 보다 효과적으로 활용할 수 있었고, 이를 통해 시스템의 최대 성능을 끌어내는 방안을 이해하게 되었습니다.
2. 대체 솔루션 탐색의 중요성
프로젝트 과정에서, 서버 피드백의 부재와 같은 기술적 제약을 겪었지만, 이를 해결하기 위해 여러 대안을 모색하고 시도했습니다. 비록 처음 의도했던 방법이 실효성이 없다는 사실을 알게 되었지만, 다양한 대체 방안을 모색하며 문제를 끝까지 해결하려는 태도가 중요하다는 것을 배웠습니다. 이 경험을 통해, 기술적 어려움이나 제약 상황에서도 포기하지 않고 문제를 해결하는 개발자적 사고를 키울 수 있었습니다.
- 끈기와 집요함: 여러 방법을 시도하며, 문제 해결을 위해 끊임없이 새로운 아이디어를 내고 실험하는 과정이 중요하다는 것을 깨달았습니다.
- 비교와 분석: 각 해결책의 장단점을 비교하고 분석하여 최적의 대안을 선택하는 능력을 키웠습니다. 이를 통해 분석적 사고와 문제 해결 능력이 강화되었습니다.
3. 사용자 경험(UX)과 기술적 솔루션의 균형
기술적 문제를 해결하는 것뿐만 아니라, 사용자 경험(UX)을 고려한 솔루션을 만드는 것이 얼마나 중요한지 배웠습니다. ProgressBar UI를 도입하여 시스템이 실제로 작업 중임을 시각적으로 표현함으로써, 사용자에게 더 나은 경험을 제공할 수 있었습니다. 기술적으로 우수한 솔루션을 개발하는 것만큼, 사용자 친화적인 기능을 제공하는 것도 서비스 품질에 중요한 요소임을 깨달았습니다.
- 시각적 피드백의 중요성: 복잡한 작업이나 대기 시간이 긴 작업에서 사용자에게 시각적인 피드백을 제공함으로써, 사용자 신뢰를 유지하고 만족도를 높일 수 있다는 점을 배웠습니다.
- 사용자 중심 사고: 기술적 해결책을 설계할 때, 사용자의 관점에서 어떻게 하면 더 나은 경험을 제공할 수 있을지 고민하게 되었으며, 이는 앞으로의 개발 과정에서 UX를 우선적으로 고려하는 사고방식을 길러 주었습니다.