중요한 건 꺾이지 않는 API (feat. Next.js API Routes)

Jiwon Han
29CM TEAM
Published in
9 min readFeb 9, 2023

안녕하세요, 29CM 콘텐츠 스쿼드에 소속된 프론트엔드 개발자 한지원입니다.

29CM 프로덕트 팀의 스쿼드는 ‘목표를 정했다면 마하 29의 속도로 실행’하는 것을 추구하는 조직입니다. 오늘은 그 스쿼드의 일원이자, 프론트엔드 개발자로서 린하게 결과물을 만들었던 경험을 콘텐츠 큐레이션 API 개발 과정을 통해 공유해보고자 합니다.

콘텐츠 큐레이션 기능이란?

29CM 앱 홈에서 ‘주간 인기 콘텐츠/ 단독 할인과 발매/ 29CM 시리즈’ 와 같이 특정 기획을 기반으로 콘텐츠를 묶어 제공하는 기능입니다.

저희 콘텐츠 스쿼드는 이 기능을 통해 기존 ‘맨/ 우먼/ 라이프’ 등의 카테고리 기반으로만 콘텐츠를 제공하던 앱 홈 화면에서 큐레이션 된 콘텐츠를 신규로 제공함으로써, 홈이 사용자에게 더욱 가치 있는 공간이 되는지, 사용자가 더 자주 방문하게 되는지를 검증하고자 하였습니다.

지원님 이번 프로젝트는 백엔드 리소스가 없습니다

스쿼드는 목적 달성을 위한 여러 실행을 동시에 진행할 수 있습니다. 그래서 필요에 따라 리소스를 선택적으로/최소한도로 활용하는 실행을 해야 할 때도 있는데요, 콘텐츠 큐레이션은 빠르게 개발하고 빠르게 검증하기 위한 MVP 로, 저희 스쿼드 내부 논의 결과 클라이언트 리소스만으로 이 제품을 개발해 보기로 하였습니다.

큐레이션 제품 구조 및 할당 리소스

그럼 저의 큐레이션 API 개발 과정을 따라가 볼까요?

1. Architecture

큐레이션 API 는 특정 카테고리를 path name 으로 받아, 해당 카테고리 기반으로 분류된 콘텐츠 정보를 반환해야 하는 API 입니다.

⚙️ API, 뭘로 개발해야 하지 ?

29CM 프론트엔드 팀은 Next.js 기반으로 웹 개발을 하고 있습니다. Next.js 는 pages/api 폴더에 파일을 생성하여 API Endpoint 를 만들 수 있는 API Routes 기능을 제공하기 때문에 하나의 프로젝트 내에서 API 개발을 진행할 수 있었습니다.

🤔 특정 기획 기반으로 29CM 의 콘텐츠를 분류할 수 있을까 ?

29CM 에서는 제품 및 유저 행동 분석 도구로 Amplitude 를 사용하고 있습니다. Amplitude 의 차트 기능을 활용하여 29CM 의 콘텐츠를 특정 카테고리 기반으로 분류할 수 있었고, Amplitude 에서 제공하는 Dashboard REST API 를 활용하여 차트의 데이터를 끌어올 수 있었습니다.

위와 같은 사항들을 고려하여 초기 큐레이션 API 구현을 위해 아래와 같은 Architecture 를 생각하게 되었습니다.

요청 수 만큼 Amplitude API 호출, 차트 데이터 반환

2. API 캐싱

그런데 개발 과정에서 문제를 발견했어요. 제가 만든 API 가 일정한 시간 내에 Response 를 가져오지 못하고 있었습니다. 어떤 요청은 600ms 의 응답 속도를 가지고, 또 다른 요청은 3,000ms 의 응답 속도를 가지는 식이었어요.

Amplitude API 호출 시 일정하지 않은 응답 속도 확인

원인은 Amplitude API 와 관련이 있어 보였습니다. 안정적이지 않은 응답 속도는 긴 로딩으로 이어져 사용자 이탈도 우려되는 상황이었어요.

이렇게 로딩이 길어지면 … 아 안돼!

이에 Amplitude API 의 응답을 캐싱하여 보다 안정적으로 응답 속도를 가져올 수 있도록 코드 개선을 진행하였습니다.

💻 TTL 기반의 단순한 Cache Class 구현하기

큐레이션 API에는 Map 객체를 활용한 단순한 구조의 캐시를 적용하였습니다. TTL이 만료된 데이터는 isStale 메소드로 확인할 수 있고, 데이터가 갱신되기 전에는 stale 한 데이터가 제공될 수 있도록 구현하였습니다.

interface CacheInterface<T> {
data: Map<string, CacheDataType<T>>;
set: (key: string, value: T, ttl: number) => void;
get: (key: string) => CacheDataType<T> | null;
isStale: (key: string) => boolean;
}


class Cache<T> implements CacheInterface<T> {

...

set(key, value, ttl) {
const insertTimeAsMs = Date.now();
const expireTimeAsMs = insertTimeAsMs + ttl;

this.data.set(key, {
value,
expireAt: expireTimeAsMs,
});
}

get(key) {
return this.data.get(key) ?? null;
}

...

}
캐시 상태에 따른 Amplitude API 호출, 캐싱된 차트 데이터 반환

3. API Preload 로 캐시 미스 줄이기

캐시도 붙였으니 이제 큐레이션 페이지가 세상의 빛을 볼 수 있나 싶었는데, 한가지 해결 못한 문제가 있었어요. 바로 캐시 미스 상태의 API 요청에서는 여전히 안정적인 응답 속도를 가질 수 없다는 것이었습니다.

💡Amplitude API 응답을 주기적으로 캐싱할 수 있다면 ?

고민 끝에 큐레이션 API 요청을 매번 고객이 하는 것이 아니라, Batch Job 으로 가져갈 수 있다면 캐시 미스를 줄일 수 있지 않을까 하는 생각이 들었습니다.

이와 관련하여 기반 서비스를 담당하는 서비스 플랫폼 팀에 문의해 본 결과, 컨테이너 외부에서 주기적으로 실행하는 Health Check 의 Endpoint 가 Next.js 애플리케이션에 있었고, 이 Endpoint 에서 주기적으로 큐레이션 Preload API 를 호출하여 Amplitude API 응답을 캐싱하는 방식으로 접근해 볼 수 있었습니다!

// cache
const cache = new Cache<T>();


// API
export default async (req: NextApiRequest, res: NextApiResponse<T>) => {
const { sendResponseBody, sendServerError, parseQueryParams } = getServerUtils(req, res);
const { cacheAmplitudeResponse, ...otherUtils } = getAmplitudeUtils();
const { preload, curation, ...otherParams } = parseQueryParams();

const url = getUrlByCurationType(curation);
const cacheKey = curation;

if (preload) {
const cacheExpireAt = cache.get(cacheKey)?.expireAt;
const isRecomputationRequired = cacheExpireAt ? cacheExpireAt - Date.now() < MINUTE : true;

if (isRecomputationRequired) {
cacheAmplitudeResponse(url, cacheKey); // asynchronous function
sendResponseBody(HTTP_STATUS.ACCEPTED, null); // 202

return;
}

sendResponseBody(HTTP_STATUS.OK, null); // 200

return;
}

...

};
큐레이션 Preload API 호출, 비동기적으로 Amplitude API 호출 및 응답 캐싱

위 개선을 통해 마지막 이슈였던 캐시 미스 이슈를 해소할 수 있었습니다.

그래서 큐레이션 페이지는 무사히 실험에 나갈 수 있었나요 ?

촘촘했던 콘텐츠 큐레이션 프로젝트 일정

네! 우여곡절이 많아 보였지만 위와 같은 API 개선 작업은 개발팀 내 여러 동료분들의 도움을 받아 빠르게 실험을 할 수 있었고, 결과적으로 콘텐츠 큐레이션 제품으로 인하여 고객이 더 자주 29CM 에 돌아온다는 것을 확인할 수 있었습니다! 🙌

콘텐츠 큐레이션 정식 제품화 된 날 🥳

마치며

사실 이번 실행을 앞두고 걱정이 앞섰었는데요, 스쿼드에서 제품을 만들기 위한 의사결정 하나하나가 이제 겨우 입사 반년 차 주니어 개발자인 제게는 모두 어렵게만 느껴졌기 때문이에요.

나 지금 잘 하고 있는 걸까 … 🫠

그러나 저의 이런 고민을 29CM 의 엔지니어링 팀에 공유했을 때 많은 동료분들이 도움을 주셨고, 덕분에 저와 저희 스쿼드가 더 나은 선택과 함께 콘텐츠 큐레이션 기능을 빠르게 런칭하고 정식 제품화를 할 수 있었다고 느꼈습니다.

혼자서 했다면 Cache Stampede 같은 건 지금도 몰랐을걸요 .. ? 🤔

이번 개발 과정에서 동료분들의 따뜻하고 적극적인 서포트와, 스쿼드에서의 빠른 실행을 위한 논의와 액션 등을 경험하고 나니 이후 스쿼드에서 일을 할 때 더 씩씩하게 개발을 할 수 있게 되지 않았나 싶습니다 :)

길지 않은 글이었지만, 저의 스쿼드 제품 개발기를 읽어 주셔서 감사합니다. 다음에도 공유하고 싶은 이야기가 있으면 또 찾아 오겠다고 다짐하며 글을 마치겠습니다 🙌

🙋🏻‍♂‍‍ 함께 성장할 동료를 찾습니다

29CM (무신사) 는 3년 연속 거래액 2배의 성장을 이루었습니다.

이제 더 큰 성장을 위해 기존 모놀리틱 서비스 구조를 마이크로서비스 구조로 전환하고, Angular 기반 프론트엔드 코드를 React 로 전환하는 등의 기술적인 시도를 진행하고 있습니다.

함께 성장하고 유저 가치를 만들어낼 동료 개발자분들을 찾습니다
많은 지원 부탁합니다!

🚀 29CM 채용 페이지 : https://www.29cmcareers.co.kr/

--

--