ChatGPT와 Node.js로 네이버 뉴스 자동 요약 시스템 만들기

2025. 5. 1. 21:16Backend Development/Node.js

반응형

요즘 뉴스가 너무 많습니다. 특히나 요즘 시국의 정치 쪽은 따라가기도 너무 벅찹니다. 그래도 이 흐름을 놓칠수야 없죠.

오늘은 네이버 뉴스를 크롤링해서 ChatGPT를 통해 내용을 요약하고, 그 내용을 읽기 편하게 텍스트 파일로 저장하는 훌륭한 자동화 프로그램을 만드려고 합니다.

 

이 글에서는 그 과정을 소개하고, 전체 코드를 공개하겠습니다.


프로젝트 개요

목표는 뉴스를 요약하고 파일 저장을 자동화 하는 겁니다. 사용할 기술 스택은 아래와 같습니다.

  • Node.js
  • Puppeteer (크롤링)
  • OpenAI API (ChatGPT)
  • fs 모듈 (파일 저장)

1. 네이버 뉴스 크롤링하기

크롬에서 개발자 도구를 키고, 네이버 뉴스의 구조를 먼저 파악해야합니다. 

네이버 뉴스 페이지

헤드라인은 10개가 있습니다. 그렇다면 우선 각 뉴스 페이지에 접근할 수 있는 Link가 필요하겠네요. 

 

맥에서는 CMD + Shift + C 를 누르면, 어떤 콘텐츠가 어떤 HTML 요소인지 시각적으로 확인할 수 있습니다. 

우선 제목을 클릭하면 기사를 이동할 수 있으니, 제목에서 어떤 정보를 가지고 있는지 확인해줍니다.

 

해당 요소를 보니 "a" 태그 안에 "href" 요소 내에 기사 Link를 가지고 있는 걸 확인할 수 있습니다. 
그러면 우리는 "a" 태그 내에 있는 "href" 요소를 모두 가져온 후, 각 페이지에 접속해서 내용을 가져오면 되는 겁니다.

 

요소를 살펴보니 "body > div > div#ct_wrap > div.ct_scroll_wrapper > div#newsct > div > div > ul > li > div > div > div.sa_text > a" 이 요소를 통해 헤드라인 10개의 a 태그에 접근할 수 있다는 걸 발견했습니다. 

 

그러면 각 뉴스 페이지를 확인해보겠습니다. 

뉴스 상세 페이지

여기서도 요소를 분석해보니, 제목은 "#title_area > span" 을 통해 가져올 수 있고, 원문은 "#dic_area" 을 통해 가져올 수 있다는 걸 알아냈습니다. 

 

그러면 단계적으로 접근하면 이렇게 되겠네요.

  1. A 태그 내에 있는 href 요소에서 링크 수집 ("body > div > div#ct_wrap > div.ct_scroll_wrapper > div#newsct > div > div > ul > li > div > div > div.sa_text > a")
  2. 수집된 링크에 접속하여, 기사 제목과 기사 내용을 수집 (제목은 "#title_area > span", 원문은 "#dic_area")

이제 코드를 작성 해보겠습니다.

 

시작하기에 앞서 기존 코드에 기반으로 코드를 추가하며, 추가된 코드는 주석으로 표시합니다.

 

1-1. 크롤링을 위한 의존성 설치

이번 작업에서는 Puppeteer를 사용할겁니다.

 

먼저 의존성을 받아줍니다.

npm install puppeteer

1-2. 네이버 뉴스 (정치) 페이지 접근

네이버 정치 뉴스 섹션인 "https://news.naver.com/section/100"에 접속을 해주고, 접속 후 페이지가 정상적으로 접속 되었는지, 스크린샷을 통해 확인해줍니다.

const puppeteer = require('puppeteer');

const naverNewsUrl = 'https://news.naver.com/section/100';

(async () => {
  const brower = await puppeteer.launch({ headless: true });
  const page = await brower.newPage();

  // 1. 섹션 페이지로 이동
  await page.goto(naverNewsUrl, { waitUntil: 'networkidle2' });
  await page.screenshot({
    fullPage: true,
    path: 'page-test.jpeg',
  });
})();

page-test.jpeg

접속 후 저장된 스크린샷을 확인 한 결과 문제 없이 잘 접근된 모양입니다.

1-3. a태그 내의 href 수집

우리가 알아낸 요소를 이용해서 뉴스 상세 페이지 링크를 수집합니다.

const puppeteer = require('puppeteer');

const naverNewsUrl = 'https://news.naver.com/section/100';

// a태그 요소
const aTagElement = 'body > div > div#ct_wrap > div.ct_scroll_wrapper > div#newsct > div > div > ul > li > div > div > div.sa_text > a';

(async () => {
  const brower = await puppeteer.launch({ headless: true });
  const page = await brower.newPage();

  await page.goto(naverNewsUrl, { waitUntil: 'networkidle2' });
  await page.screenshot({
    fullPage: true,
    path: 'page-test.jpeg',
  });
  
  // 2. 헤드라인 별 링크 저장
  const aTagList = await page.$$eval(
    aTagElement,
    ele => ele.map(e => e.href),
  );
  console.log(aTagList);
})();

 

A 태그에 있는 링크를 정상적으로 잘 가져오는지 확인하기 위해 console.log 로 배열을 출력합니다.

 

console.log(aTagList) 의 결과

1-4. 뉴스별 제목, 내용 저장

링크를 수집했으니, 각 뉴스 상세 페이지에 접근해서 뉴스 제목과 뉴스 내용을 수집합니다.

const puppeteer = require('puppeteer');

const naverNewsUrl = 'https://news.naver.com/section/100';

// 뉴스 내용을 저장하기 위한 배열
const newsArray = [];

const aTagElement = 'body > div > div#ct_wrap > div.ct_scroll_wrapper > div#newsct > div > div > ul > li > div > div > div.sa_text > a';

(async () => {
  const brower = await puppeteer.launch({ headless: true });
  const page = await brower.newPage();

  await page.goto(naverNewsUrl, { waitUntil: 'networkidle2' });
  await page.screenshot({
    fullPage: true,
    path: 'page-test.jpeg',
  });
  
  const aTagList = await page.$$eval(
    aTagElement,
    ele => ele.map(e => e.href),
  );
  console.log(aTagList);
  
  // 3. 뉴스별 제목, 내용 저장
  for (const link of aTagList) {
    await page.goto(link, { waitUntil: 'networkidle2' });
    const title = await page.$eval(
      '#title_area > span',
      el => el.innerText,
    );
    const content = await page.$eval(
      '#dic_area',
      el => el.innerText,
    );
    newsArray.push({ title, content, link });
  }
  console.log(newsArray);
})();

console.log(newsArray) 결과

title, content, link 가 잘 저장되고 있는 모습입니다.

 

이제 크롤링을 통해 뉴스를 모두 수집하였고, 정상적으로 가져오는 것까지 확인했습니다.

2. ChatGPT API를 이용해서 뉴스 내용 요약

ChatGPT API를 Node.js에서 이용하는 방법은 이전에 아래에서 소개한 적 있으므로 확인해보시는 것을 추천드립니다.

2025.04.29 - [Backend Development/Node.js] - Node.js로 ChatGPT API 연동합시다.

 

Node.js로 ChatGPT API 연동합시다.

ChatGPT를 웹 페이지나 앱에서 질문하고 답변을 받는 건 많이들 이용하고 계실겁니다.하지만 그 훌륭한 도구가 우리의 웹 애플리케이션이나 우리의 웹 사이트에 장착된다면 큰 효과는 더 뛰어나

imwh0im.tistory.com

 

ChatGPT API를 이용하는 방법은 매우 간단합니다. API Key와 간단한 프롬프트 그리고 신용카드만 있으면 누구나 가능합니다.

const puppeteer = require('puppeteer');
// openAI 의존성
const { OpenAI } = require('openai');

// OpenAI 초기화 및 API Key 입력
const openAi = new OpenAI({
  apiKey: 'API Key',
});

const naverNewsUrl = 'https://news.naver.com/section/100';

const newsArray = [];

const aTagElement = 'body > div > div#ct_wrap > div.ct_scroll_wrapper > div#newsct > div > div > ul > li > div > div > div.sa_text > a';

(async () => {
  const brower = await puppeteer.launch({ headless: true });
  const page = await brower.newPage();

  await page.goto(naverNewsUrl, { waitUntil: 'networkidle2' });
  await page.screenshot({
    fullPage: true,
    path: 'page-test.jpeg',
  });
  
  const aTagList = await page.$$eval(
    aTagElement,
    ele => ele.map(e => e.href),
  );
  console.log(aTagList);
  
  for (const link of aTagList) {
    await page.goto(link, { waitUntil: 'networkidle2' });
    const title = await page.$eval(
      '#title_area > span',
      el => el.innerText,
    );
    const content = await page.$eval(
      '#dic_area',
      el => el.innerText,
    );
    newsArray.push({ title, content, link });
  }
  console.log(newsArray);
  
   // 4. ChatGPT 에게 요약 요청
  const response = await openAi.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages: [
      { role: 'system', content: 'title, content, link 오브젝트로 이루어진 배열을 전달합니다.' },
      { role: 'system', content: 'title 은 뉴스의 제목, content 는 뉴스의 내용, link 는 뉴스의 원본 링크 입니다.' },
      { role: 'system', content: '각 뉴스별로 요약하여, markdown 파일을 주세요.' },
      { role: 'system', content: '요약 방법은 다음과 같습니다. 뉴스 제목, 요약 내용, 원본 뉴스 링크, 주요 키워드 입니다.' },
      { role: 'system', content: '요약 내용 외의 다른 응답을 해주시지 말아 주세요.' },
      { role: 'user', content: JSON.stringify(newsArray) },
    ],
    temperature: 0.4,
  });

  console.log(response.choices[0].message.content);
})();

 

발급된 API Key 를 입력하여, OpenAI 클래스를 초기화해준 후, 적절한 지침을 제공해서 좋은 답변을 받을 수 있도록 합니다.

그 후 뉴스의 제목, 내용, 링크가 담긴 "newsArray" 를 JSON.stringify 해준 후 API를 보냅니다.

Tip1: 모델의 선택 이유는 3.5 가 더 많은 토큰을 지원하기 때문에, 뉴스 같이 큰 텍스트를 다루는 데 유리합니다.
Tip2: temperature 는 ChatGPT의 창의성 지표라고 보시면 됩니다. 취향 차이이지만 저는 평소에는 0.6 정도를 이용하고, 요약이나 번역 같은 창의성이 비교적 필요없는 작업에서는 0.4 이하로 설정합니다. 

console.log(response.choices[0].message.content); 의 결과

3. 요약 결과 파일로 저장

Node.js 내장 모듈이 fs를 이용해서 파일을 핸들링합니다.

const puppeteer = require('puppeteer');
const { OpenAI } = require('openai');
// fs 의존성
const fs = require('fs');

const openAi = new OpenAI({
  apiKey: 'API Key',
});

const naverNewsUrl = 'https://news.naver.com/section/100';

const newsArray = [];

const aTagElement = 'body > div > div#ct_wrap > div.ct_scroll_wrapper > div#newsct > div > div > ul > li > div > div > div.sa_text > a';

(async () => {
  const brower = await puppeteer.launch({ headless: true });
  const page = await brower.newPage();

  await page.goto(naverNewsUrl, { waitUntil: 'networkidle2' });
  await page.screenshot({
    fullPage: true,
    path: 'page-test.jpeg',
  });
  
  const aTagList = await page.$$eval(
    aTagElement,
    ele => ele.map(e => e.href),
  );
  console.log(aTagList);
  
  for (const link of aTagList) {
    await page.goto(link, { waitUntil: 'networkidle2' });
    const title = await page.$eval(
      '#title_area > span',
      el => el.innerText,
    );
    const content = await page.$eval(
      '#dic_area',
      el => el.innerText,
    );
    newsArray.push({ title, content, link });
  }
  console.log(newsArray);
  
  const response = await openAi.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages: [
      { role: 'system', content: 'title, content, link 오브젝트로 이루어진 배열을 전달합니다.' },
      { role: 'system', content: 'title 은 뉴스의 제목, content 는 뉴스의 내용, link 는 뉴스의 원본 링크 입니다.' },
      { role: 'system', content: '각 뉴스별로 요약하여, markdown 파일을 주세요.' },
      { role: 'system', content: '요약 방법은 다음과 같습니다. 뉴스 제목, 요약 내용, 원본 뉴스 링크, 주요 키워드 입니다.' },
      { role: 'system', content: '요약 내용 외의 다른 응답을 해주시지 말아 주세요.' },
      { role: 'user', content: JSON.stringify(newsArray) },
    ],
    temperature: 0.4,
  });

  console.log(response.choices[0].message.content);
  
  // 5. 요약 글 파일로 저장
  fs.writeFile('summary.md', response.choices[0].message.content, 'utf8', (err) => {
    if (err) {
      console.error('파일 쓰기 실패', err);
      return;
    }
    console.log('요약 파일이 생성되었습니다.');
  });
})();

summary.md 파일 결과

요약 파일이 정상적으로 저장된 모습입니다.

 

이 markdown 을 렌더링하면 아래와 같이 읽기 좋은 형태의 글로 보입니다. 

markdown

4. 추가 기능

이제 여기서 하루에 한번씩 스케줄링을 걸수도 있고, 만들어진 파일을 이메일로 보내게끔 할수 있습니다.

이러한 기능이 누군가에게 필요하고 편의를 제공할 수 있다면 그것 또한 작은 서비스가 될수 있습니다.

 

전체 코드는 아래와 같습니다.

https://github.com/imwh0im/crawler-naver-news

 

GitHub - imwh0im/crawler-naver-news: naver news crawling and make summary

naver news crawling and make summary. Contribute to imwh0im/crawler-naver-news development by creating an account on GitHub.

github.com

 

 

마무리

ChatGPT를 통해 특정 내용을 요약시키고, 그걸 파일로 저장하는 과정을 진행했습니다.

채팅기능도 훌륭한 도구이지만, API로써 내 서비스나 내 필요에 따라 사용한다면 더 높은 가치를 줄 수 있는 도구입니다.

요약, 번역 그리고 트렌드 분석 등 다양하게 사용해보시는 건 어떠세요?

 

비슷한 다른 글

2025.04.30 - [AI] - 중국발 초신성 Qwen3, 왜 주목해야 할까요?

 

중국발 초신성 Qwen3, 왜 주목해야 할까요?

인공지능 모델 이름이 하루가 멀다 하고 바뀌고 있는 요즘, 알리바바 클라우드가 공개한 Qwen3가 커뮤니티를 뜨겁게 달구고 있습니다. 모델 숫자부터 사양까지 매우 복잡해 보이지만, 꼭 알아둘

imwh0im.tistory.com

 

 

반응형