네이버 키워드 검색 API 성능 최적화하기: 7분에서 1분 이내로 단축한 방법

Khan·2025년 3월 14일
1

문제 상황

현재 개발하고 있는 서비스는 사용자가 입력한 키워드의 검색량을 조회해주는 기능을 제공하고 있다. 최대 100개까지 한 번에 검색할 수 있는데, 이 과정이 너무 오래 걸린다는 사용자 피드백이 지속적으로 들어왔다. 실제로 100개의 키워드를 검색하면 약 7분이라는 긴 시간이 소요되었다.

문제를 분석해보니 다음과 같은 원인이 있었다:

  1. 순차적 처리: 키워드를 하나씩 순차적으로 처리하고 있었다.
  2. 중복 API 호출: 각 키워드마다 별도의 API 호출이 발생했다.
  3. 비효율적인 데이터 관리: 결과를 개별적으로 처리하고 상태를 업데이트했다.

이러한 문제를 해결하기 위해 코드를 리팩토링하기로 결정했다.

해결 방법

1. 배치 처리 도입

가장 먼저 키워드를 개별적으로 처리하는 대신 '배치(batch)'로 묶어서 처리하는 방식을 도입했다. 배치 처리란 여러 작업을 그룹으로 묶어 한 번에 처리하는 방법이다.

// 기존 방식: 키워드 하나씩 처리
const processKeyword = async (keyword) => {
  const result = await fetchKeywordData(keyword);
  // 결과 처리
};

// 개선된 방식: 여러 키워드를 배치로 처리
const processBatch = async (keywords) => {
  const results = await fetchKeywordDataBatch(keywords);
  // 여러 결과 한 번에 처리
};

이를 위해 새로운 API 엔드포인트를 만들었다:

  • /api/keywords/batch: 여러 키워드를 한 번에 처리
  • /api/naver-datalab/batch: 여러 데이터랩 요청을 동시에 처리

이 API들은 기존의 단일 키워드 처리 API를 활용하되, 여러 키워드를 한 번에 요청할 수 있도록 수정한 것이다.

2. 병렬 처리 구현

다음으로 Promise.all을 활용해 여러 API 요청을 병렬로 처리하도록 구현했다. 병렬 처리는 여러 작업을 동시에 실행하여 총 소요 시간을 줄이는 방법이다.


// 기존 방식: 순차 처리
for (const keyword of keywords) {
  await processKeyword(keyword);
}

// 개선된 방식: 병렬 처리
const batchSize = 5; // 한 번에 처리할 키워드 수
for (let i = 0; i < keywords.length; i += batchSize) {
  const batch = keywords.slice(i, i + batchSize);
  await Promise.all(batch.map(keyword => processKeyword(keyword)));
}

특히 네이버 API의 특성을 고려하여 최적의 배치 크기를 5개로 설정했다. 이는 API 서버의 부하와 클라이언트 처리 능력 사이의 균형점이다.

3. 상태 관리 최적화

기존에는 각 키워드 결과가 도착할 때마다 상태를 업데이트했는데, 이 방식은 불필요한 렌더링을 많이 발생시킨다. 개선된 코드에서는 배치 단위로 상태를 업데이트하도록 변경했다.

// 기존 방식: 개별 결과 추가
const addKeywordResult = (result) =>
  set((state) => ({
    keywordResults: [...state.keywordResults, result],
  }));

// 개선된 방식: 여러 결과 한 번에 추가
const addKeywordResults = (results) =>
  set((state) => ({
    keywordResults: [...state.keywordResults, ...results],
  }));

이렇게 함으로써 상태 업데이트 횟수를 크게 줄일 수 있었다.

4. 캐싱 전략 도입

동일한 키워드에 대한 중복 요청을 방지하기 위해 간단한 메모리 캐싱 시스템을 구현했다. 이미 검색한 키워드는 캐시에서 바로 결과를 가져올 수 있도록 했다.

// 캐싱 상태 추가
cachedResults: new Map(),

// 결과 캐싱 로직
addKeywordResult: (result) =>
  set((state) => ({
    keywordResults: [...state.keywordResults, result],
    cachedResults: new Map(state.cachedResults).set(result.keyword, result)
  })),

이는 사용자가 같은 키워드를 반복해서 검색할 때 성능을 크게 향상시킨다.

성능 개선 결과

리팩토링 후 성능 테스트를 진행한 결과, 100개 키워드 검색 시간이 7분에서 약 1분 이내로 단축되었다. 이는 약 85% 이상의 성능 향상을 의미한다.

특히 다음과 같은 측면에서 개선이 이루어졌다:

  1. API 호출 횟수 감소: 100개 키워드를 처리하기 위해 필요한 API 호출이 100회에서 약 20회로 감소
  2. 병렬 처리로 인한 시간 단축: 여러 키워드를 동시에 처리하여 대기 시간 최소화
  3. 상태 업데이트 최적화: 렌더링 횟수 감소로 UI 반응성 향상
  4. 안정성 증가: 타임아웃 처리와 오류 대응 체계 개선

개선 과정에서 마주친 도전과 해결책

네이버 API 제한 문제

네이버 API는 초당 요청 수 제한이 있어서 너무 많은 병렬 요청을 보내면 오류가 발생할 수 있다. 이 문제를 해결하기 위해 적절한 배치 크기(5개)를 선택하고, 배치 간에 약간의 지연을 추가했다.

// 다음 배치 처리 전 잠시 대기
setTimeout(processBatch, 10);

메모리 사용량 관리

배치 크기가 커질수록 메모리 사용량도 증가한다. 이 트레이드오프를 관리하기 위해 최적의 배치 크기를 찾는 테스트를 여러 번 진행했다.

오류 처리 개선

배치 처리 중 일부 요청이 실패할 경우에도 전체 검색이 중단되지 않도록 오류 처리 로직을 강화했다.

try {
  // API 요청 처리
} catch (error) {
  // 실패한 요청에 대한 결과 생성
  batchResultsRef.current.set(keyword, {
    keyword,
    keywordData: null,
    pcData: null,
    mobileData: null
  });
}

배운게 된 내용

이번 리팩토링을 통해 배우게 된 핵심 포인트는 다음과 같다:

  1. 단일 처리보다 배치 처리가 효율적이다: 특히 네트워크 요청이 많은 경우 배치 처리는 큰 성능 향상을 가져온다.
  2. 병렬 처리의 중요성: JavaScript의 비동기 특성을 잘 활용하면 성능을 크게 개선할 수 있다.
  3. 사용자 경험을 고려한 설계: 긴 처리 시간이 필요한 작업에서는 진행 상황을 명확히 표시하는 것이 중요하다.
  4. 오류 대응 체계의 중요성: 배치 처리에서는 개별 요청의 실패가 전체 작업에 영향을 미치지 않도록 설계해야 한다.

이런 최적화는 단순히 코드 개선에 그치지 않고 사용자 경험을 크게 향상시킨다. 검색 시간이 짧아지면서 사용자는 더 많은 키워드를 더 빠르게 분석할 수 있게 되었다.

앞으로도 서비스 성능을 지속적으로 모니터링하고 개선해 나갈 계획이다. 다음 단계로는 서버 사이드 캐싱이나 웹 워커를 활용한 추가 최적화를 고려하고 있다.

profile
끄적끄적 🖋️

0개의 댓글