지난 포스트에서 네이버 뉴스에 카테고리를 지정하는 메서드를 구현했었다.
이번 포스트에서는 네이버 뉴스에 카테고리를 지정 후 요청 파라미터와 일치하는 카테고리를 가진 뉴스를 반환하는 로직을 구현해보자.
전체 코드
async fetchByCategory(category: Category): Promise<NewsItem[]> {
try {
const query = CATEGORY_QUERY_MAP[category] ?? '뉴스';
const result: NewsItem[] = [];
let start = 1;
const FETCH_SIZE = 10;
const TOTAL_ITEMS = 100;
const MAX_START = 1000;
while (result.length < TOTAL_ITEMS && start <= MAX_START) {
const response = await firstValueFrom(
this.httpService.get<NewsResponse>(
'https://openapi.naver.com/v1/search/news.json',
{
params: { query, display: FETCH_SIZE, start, sort: 'date' },
headers: {
'X-Naver-Client-Id': this.clientId,
'X-Naver-Client-Secret': this.clientSecret,
},
},
),
);
const items = response.data.items;
if (!items.length) break;
const classifiedItems = await Promise.allSettled(
items.map(async (item) => {
const [{ thumbnailUrl, companyName }, classifiedCategory] =
await Promise.all([
this.fetchMetadata(item.link),
this.categorizeNews(item.title, item.description),
]);
return {
...item,
title: this.decodeHtmlEntities(item.title).replace(
/<[^>]*>/g,
'',
),
description: this.decodeHtmlEntities(item.description).replace(
/<[^>]*>/g,
'',
),
thumbnailUrl,
companyName,
category: classifiedCategory,
};
}),
);
const matched = classifiedItems
.filter(
(r): r is PromiseFulfilledResult<NewsItem> =>
r.status === 'fulfilled',
)
.map((r) => r.value)
.filter((item) => item.category === category);
result.push(...matched);
start += FETCH_SIZE;
}
return result.slice(0, 100);
} catch {
throw new InternalServerErrorException(
'카테고리별 뉴스 API 호출에 실패했습니다.',
);
}
}
조금 긴데, 하나씩 뜯어보자.
let start = 1;
const FETCH_SIZE = 10;
const TOTAL_ITEMS = 100;
const MAX_START = 1000;
while (result.length < TOTAL_ITEMS && start <= MAX_START) {
...
}
100개의 아이템을 10개씩 반환하여 10페이지를 만드는 페이지네이션을 구현해 반환할 것이다.
1부터 시작해서 지정된 카테고리 뉴스들을 100까지 채울 때까지 순회하며, 요청 1000개 초과 시 종료한다.
const response = await firstValueFrom(
this.httpService.get<NewsResponse>(
'https://openapi.naver.com/v1/search/news.json',
{
params: { query, display: FETCH_SIZE, start, sort: 'date' },
headers: {
'X-Naver-Client-Id': this.clientId,
'X-Naver-Client-Secret': this.clientSecret,
},
},
),
);
firstValueFrom으로 RxJS Observable을 Promise로 변환한다. sort: 'date'로 최신순으로 10개씩 가져온다.
const classifiedItems = await Promise.allSettled(
items.map(async (item) => {
const [{ thumbnailUrl, companyName }, classifiedCategory] =
await Promise.all([
this.fetchMetadata(item.link),
this.categorizeNews(item.title, item.description),
]);
return {
...
};
}),
);
각 아이템마다 Promise.all()로 병렬 처리를 해놓았는데, fetchMetadata로 썸네일과 언론사명을 가져오고, categorizeNews(저번 포스팅에서 구현했던) 으로 카테고리를 분류한다. 10개의 아이템들을 Promise.allSettled()로 쓴 이유는 하나를 실패하더라도, 나머지 결과를 살리기 위해서이다.
const matched = classifiedItems
.filter(
(r): r is PromiseFulfilledResult<NewsItem> =>
r.status === 'fulfilled',
)
.map((r) => r.value)
.filter((item) => item.category === category);
AI가 분류한 카테고리가 요청 파라미터로 들어온 카테고리와 일치하는 값만 남긴다.
result.push(...matched);
start += FETCH_SIZE;
결과 값을 push 하고 start를 10 증가 시킨다.
카테고리 분류 프롬프트 수정
하다보니 Groq AI가 오분류하여 값이 제대로 반환되지 않는 경우가 발생하였다.
[fetch] start=981, 가져온 개수=10
[classify] 분류 결과: ['SOCIETY', 'SOCIETY', 'SOCIETY', ...]
[filter] 요청 카테고리=ECONOMY, 일치=0개
이런식으로.. 981개까지 돌아야 겨우 94개를 채우는 문제가 발생했다.
그래서 프롬프트를 수정하여 기준을 더 구체화시켰다.
messages: [
{
role: 'system',
content: `당신은 뉴스 카테고리 분류기입니다. 반드시 아래 기준에 따라 카테고리 이름 하나만 반환하세요. 다른 텍스트는 절대 포함하지 마세요.
[카테고리 분류 기준]
- POLITICS: 국회, 정당, 선거, 대통령, 장관, 외교, 법안, 정책 결정 등 정치 권력과 관련된 내용
- ECONOMY: 주식, 환율, 금리, 기업 실적, 산업, 무역, 부동산 시장, 경제 지표, 고용/취업 등 경제 활동과 관련된 내용. 물가·소비·서민 경제도 ECONOMY에 해당
...
[경계 기준]
- 물가·소비·서민경제 → ECONOMY (SOCIETY 아님)
- 국내 정치인의 경제 정책 발언 → POLITICS
...
},
{
role: 'user',
content: `다음 뉴스를 분류해주세요.
제목: ${title}
내용: ${description}
카테고리 이름만 반환하세요.`,
},
],
Claude의 도움을 받아서 완성했다. 길어서 다 포스트에 다 담진 않았지만 프롬프트를 수정하고 나니 100개를 채울 수 있었다.
다음 포스트에서는 시간이 오래 걸리는 크롤링을 위해 캐싱한 내용을 작성하겠다.

감사합니다 ^_~
'Development > Back-end' 카테고리의 다른 글
| [NestJS] 네이버 뉴스 카테고리 지정하기 (3) (0) | 2026.03.28 |
|---|---|
| [NestJS] 네이버 뉴스 카테고리 지정하기 (1) (0) | 2026.03.28 |
| [NestJS] Pipe 사용하기 (1) | 2026.02.26 |
| [Spring Boot] 어댑터 패턴 구현하기(with. JPA, Redis) (0) | 2025.12.26 |
| [Spring Boot] @Data 알고 쓰기 (0) | 2025.11.26 |
