[NestJS] 네이버 뉴스 카테고리 지정하기 (2)

2026. 3. 28. 12:12·Development/Back-end

지난 포스트에서 네이버 뉴스에 카테고리를 지정하는 메서드를 구현했었다.

이번 포스트에서는 네이버 뉴스에 카테고리를 지정 후 요청 파라미터와 일치하는 카테고리를 가진 뉴스를 반환하는 로직을 구현해보자.

 

전체 코드

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
'Development/Back-end' 카테고리의 다른 글
  • [NestJS] 네이버 뉴스 카테고리 지정하기 (3)
  • [NestJS] 네이버 뉴스 카테고리 지정하기 (1)
  • [NestJS] Pipe 사용하기
  • [Spring Boot] 어댑터 패턴 구현하기(with. JPA, Redis)
knhye
knhye
  • 전체
    오늘
    어제
  • knhye
    3n1hye_
    knhye
  • 링크

    • GitHub
    • 분류 전체보기 (61)
      • Development (28)
        • Back-end (21)
        • DB (3)
        • CS (4)
      • Algorithm (6)
      • DevOps (10)
        • git (1)
        • Cloud Platform (5)
        • CICD (1)
        • Cloud Native (2)
      • Internet (2)
      • 매일메일 (6)
      • 회고 (5)
        • Capstone (2)
        • Hackathon (1)
        • 2025 (2)
      • 자격증 (1)
      • 블로그 리딩 (3)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
knhye
[NestJS] 네이버 뉴스 카테고리 지정하기 (2)
상단으로

티스토리툴바