📉국내 주식시장에서 ‘테마주’를 미리 발굴하려면, 뉴스의 흐름을 빠르게 캐치하고 종목에 연결시키는 게 핵심(구글시트-> 실시간뉴스)

반응형

1. 전체 전략 개요

  1. 뉴스 수집 자동화
    • 해외/국내 주요 언론사에서 경제·산업·정책 관련 기사 자동 수집
  2. 핵심 키워드 추출 & 알림
    • 기사에서 새로 부각되는 키워드(예: AI반도체, 수소, 전기차 보조금) 자동 추출
  3. 테마 연결
    • 키워드 → 관련 산업 → 관련 국내 상장사 매핑
  4. 실시간 모니터링 & 필터링
    • 종목별 주가/거래량 급등락과 뉴스 발생 시점 비교
  5. 테마 랭킹 & 포트폴리오 업데이트
    • 언급량·거래량·기관/외국인 수급 지표를 합쳐 점수화

2. 뉴스 소스 추천

📌 해외 뉴스

  • Bloomberg, Reuters, CNBC, WSJ – 글로벌 정책·기업 뉴스
  • TechCrunch / The Verge – 신기술·AI·IT 스타트업 소식
  • OilPrice.com, S&P Global – 원자재·에너지 테마
  • US DoE·EU Commission 발표자료 – 정책·규제 방향성

📌 국내 뉴스

  • 연합뉴스, 뉴스1, 머니투데이, 이데일리, 조선비즈, 매일경제, 한국경제
  • DART 공시 – 기업 공시(수주·실적·지분변동)
  • 산업부·환경부·금융위 보도자료 – 정책 테마주 선제 대응

✅ 팁: 해외발(특히 미국/유럽 정책) 뉴스가 국내 테마주 급등의 선행지표가 되는 경우가 많습니다.
예: 미국 IRA법 → 국내 2차전지/수소주 → 국내 뉴스 뒤따름


3. 수집 & 자동화 방법

(1) RSS 활용

대부분의 언론사·기관은 RSS 피드를 제공합니다.

(2) 뉴스 API

  • NewsAPI.org(영/한 기사 수집 가능)
  • 네이버 뉴스 API(네이버 개발자센터)
  • 키워드 필터: "AI" OR "전기차" OR "수소" 등 설정

(3) 웹 스크래핑

  • 파이썬 BeautifulSoup + requests 사용
  • 특정 언론사의 산업/증권 섹션 크롤링
  • 하루 1~2회 크론탭(스케줄러)로 자동 실행

4. 키워드 → 테마 매핑

  1. 텍스트 분석으로 키워드 추출
    • KeyBERT / KoNLPy / Keyphrase Extraction
    • 새로 부각되는 키워드(예: “AI 반도체”, “차세대 배터리”, “폐배터리 재활용”)
  2. 테마 사전 구축
    • 예) 수소 → 효성첨단소재, 일진하이솔루스, 풍국주정
    • AI 반도체 → 한미반도체, 제주반도체, 리노공업
    • 한 번 만들어두면 새로운 키워드가 등장했을 때 즉시 종목 매칭 가능

5. 실시간 모니터링

  • 네이버금융·Investing.com 알림: 특정 종목 급등/뉴스 발생 시 알림
  • Fnguide / DART: 공시 속보
  • 증권사 HTS: 실시간 뉴스 + 키워드 알림 기능 있음
  • 트위터(X)·레딧 r/stocks: 급등 전 테마 힌트가 올라올 때 많음

6. 자동화 예시 (간단한 흐름) 시간

graph TD
A[RSS/NewsAPI로 기사 수집] --> B[Google Sheets 저장]
B --> C[Python: 키워드 추출 및 필터]
C --> D[테마/종목 매핑 DB]
D --> E[이상 거래량 종목 모니터링]
E --> F[텔레그램/카카오톡 알림]

7. 팁 & 주의사항

  • 단순 뉴스량보다 정책·수출계약·원자재가격 뉴스가 강력한 테마 촉발 요인
  • “루머성 뉴스”는 피하고 공시, 정부자료, 해외정책 우선
  • 백테스트로 “뉴스 발생 후 5일/10일 수익률”을 체크하면 테마 필터링 정교화 가능

🔥 요약

  • 해외 → 국내 순으로 뉴스 캐치
  • RSS/API로 수집 → 키워드 추출 → 테마·종목 매핑 자동화
  • 정책·산업 키워드 중심 모니터링
  • 급등락 종목과 뉴스 발생 시점 연결

구글시트 검색화면
구글 시트 로그인
구글시트 탭 이름 확인

 

아래는 구글 시트 → Apps Script → RSS → 티스토리/텔레그램 알림으로 이어지는 자동화 전체 코드 예시입니다.

 

최대한 초보자도 따라할 수 있게 단계별로 설명과 함께 붙였습니다.


🟢 1. 개요

목표:

  1. RSS 뉴스 → 구글 시트 자동 저장
  2. 시트에서 특정 키워드(테마) 필터링
  3. 필터된 뉴스 → 티스토리 자동 포스팅
  4. 동시에 텔레그램으로 푸시 알림 전송

🟢 2. 준비물

  • 구글 계정 + 구글 시트
  • 티스토리 계정 (+ 티스토리 Open API)
  • 텔레그램 Bot (@BotFather로 토큰 발급)
  • 주요 RSS 피드 URL (예: CNBC, 연합뉴스, 이데일리 등)

🟢 3. 구글 시트 설정

  1. 새 시트를 만듭니다.
  2. 시트 이름을 NewsFeed로 변경
  3. A열: 날짜, B열: 제목, C열: 링크, D열: 요약, E열: 테마(자동 분류용)

🟢 4. Apps Script 열기

  • [시트] → 확장 프로그램 → Apps Script 클릭
  • 아래 코드를 붙여넣고 저장합니다.

구글시트 확장프로그램->Apps Script 선택

 

/***** 환경설정 *****/
const RSS_FEEDS = [
  "https://www.cnbc.com/id/100003114/device/rss/rss.html", // CNBC
  "https://www.yna.co.kr/rss/economy.xml",                  // 연합뉴스 경제
  "https://www.edaily.co.kr/rss/stock.xml",                 // 이데일리 증권
];
const KEYWORDS = ["AI", "수소", "전기차", "반도체", "정책", "배터리"]; // 필터 키워드

function fetchRssToSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");
  
  RSS_FEEDS.forEach(url => {
    const xml = UrlFetchApp.fetch(url).getContentText();
    const document = XmlService.parse(xml);
    const items = document.getRootElement().getChildren("channel")[0].getChildren("item");

    items.forEach(item => {
      const title = item.getChildText("title");
      const link = item.getChildText("link");
      const pubDate = item.getChildText("pubDate");
      const description = item.getChildText("description");

      // 중복 체크
      const existing = sheet.getRange(2, 2, sheet.getLastRow(), 1).getValues().flat();
      if (existing.includes(title)) return;

      // 테마 키워드 탐색
      let theme = KEYWORDS.find(k => title.includes(k) || description.includes(k)) || "기타";

      sheet.appendRow([pubDate, title, link, description, theme]);
    });
  });
}

위 코딩을 적용한 이미지 화면

🟩 4.2 티스토리 포스팅

  1. Tistory Open API에서 애플리케이션 등록
  2. OAuth 인증 후 access_token 확보
  3. 블로그 이름(blogName) 확인
/***** Tistory API 설정 *****/
const TISTORY_ACCESS_TOKEN = "여기에_티스토리_액세스_토큰";
const TISTORY_BLOG_NAME = "내블로그명";

function postToTistory() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");
  const lastRow = sheet.getLastRow();
  
  // 가장 최근 뉴스 5개만 포스팅
  for (let i = lastRow; i > lastRow - 5; i--) {
    const [date, title, link, desc, theme] = sheet.getRange(i, 1, 1, 5).getValues()[0];
    
    const payload = {
      access_token: TISTORY_ACCESS_TOKEN,
      output: "json",
      blogName: TISTORY_BLOG_NAME,
      title: `[${theme}] ${title}`,
      content: `<p>${desc}</p><p><a href="${link}" target="_blank">기사 바로가기</a></p>`,
      visibility: 3 // 3:발행
    };

    UrlFetchApp.fetch("https://www.tistory.com/apis/post/write", {
      method: "post",
      payload: payload
    });
  }
}

🟩 4.3 텔레그램 알림

  1. BotFather → /newbot으로 Bot 생성 → API 토큰 복사
  2. 텔레그램에서 @bot_username을 시작 → https://api.telegram.org/bot/getUpdates로 chat_id 확인
/***** 텔레그램 설정 *****/
const TELEGRAM_TOKEN = "여기에_텔레그램_봇_토큰";
const TELEGRAM_CHAT_ID = "여기에_채팅_ID";

function sendTelegramAlert() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");
  const lastRow = sheet.getLastRow();
  const [date, title, link, desc, theme] = sheet.getRange(lastRow, 1, 1, 5).getValues()[0];

  const message = `🚀 [${theme}] ${title}\n${link}`;
  
  const url = `https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage`;
  const payload = {
    chat_id: TELEGRAM_CHAT_ID,
    text: message,
    disable_web_page_preview: false
  };

  UrlFetchApp.fetch(url, {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload)
  });
}

🟩 4.4 트리거 설정

  1. Apps Script → 시계 아이콘(트리거) 클릭
  2. fetchRssToSheet → 30분마다 실행
  3. postToTistory → 하루 1회 실행
  4. sendTelegramAlert → 매 실행 시마다 호출

🟢 5. 워크플로우

  1. RSS 뉴스 → 구글 시트 저장
  2. 키워드 필터로 테마 분류
  3. 최신 뉴스 5개 티스토리 자동 포스팅
  4. 새 뉴스가 들어올 때마다 텔레그램 푸시

🟢 6. 확장 아이디어

  • Sentiment 분석으로 긍/부정 뉴스 필터링
  • 거래량·주가 API와 연동 → 뉴스+주가 급등 종목만 알림
  • 번역 API(예: 구글 번역) 연결 → 해외뉴스 한글 자동 변환
  • 티스토리 SEO용 태그 자동 추가 (tag 파라미터 활용)

실행을 눌렀을경우 승인이 필요하다고 나올때

지금 보신 “Google에서 확인하지 않은 앱” 경고는 정상적인 과정입니다.
Apps Script로 처음 URLFetch, 시트 접근 등을 하면 구글이 권한 검토(승인) 를 요구합니다.
개발자가 본인일 때는 무시하고 계속 진행해도 됩니다.

아래 순서대로 하면 됩니다.


🔑 1. 경고창 해결 방법

  1. 실행 클릭 → “승인 필요” → 권한 검토 클릭
  2. “Google에서 확인하지 않은 앱” 경고창이 뜨면
    • 하단의 ‘고급’ 링크를 클릭
    • ‘프로젝트명(NewsFeed)으로 이동(안전하지 않음)’ 클릭
  3. 구글 계정을 선택하고
    • “이 앱이 Google 계정의 데이터에 액세스하도록 허용” → 허용
  4. 완료 후 다시 실행 → 이번엔 오류 없이 동작할 겁니다.

⚠️ 주의: 이 앱은 본인이 작성한 코드이므로 안심하고 승인하면 됩니다.
외부에서 받은 코드라면 반드시 코드 내용을 확인한 뒤 승인해야 합니다.


🟢 2. 권한 요청 이유

  • SpreadsheetApp → 시트 읽기/쓰기 권한
  • UrlFetchApp → 외부 RSS 가져오기
  • XmlService → XML 파싱

모두 뉴스 수집 및 저장에 필요한 정상 권한입니다.


🟢 3. 실행 순서 체크

  1. 시트에 NewsFeed 시트가 있어야 함
  2. fetchRssToSheet() 함수를 선택 후 실행
  3. 승인 완료 후 실행하면 시트에 RSS 뉴스가 자동으로 추가됩니다.

🟢 4. 추가 팁

  • 첫 실행 시 1~2개 RSS만 넣고 테스트하면 오류 찾기 쉽습니다.
  • 권한 승인 후 트리거 설정(예: 30분마다 실행)까지 해두면 자동화 완성.

👉 따라서 “고급 → (프로젝트명)으로 이동 → 허용” 경로로 승인하시면 됩니다.
실행 후 시트에 데이터가 들어오면 성공입니다.

1) Apps Script 편집기 열기 (만약 아직 안 하셨다면)

  1. 구글 스프레드시트에서 상단 메뉴 → 확장 프로그램 → Apps Script 클릭
    (또는 https://script.google.com 에서 프로젝트 열기)

2) 저장(저장 아이콘 누르기)

  • 코드 붙여넣고 나서 저장 아이콘(디스크 모양) 또는 Ctrl+S로 저장하세요.
    저장하지 않으면 최신 코드가 실행되지 않습니다.

3) 실행할 함수 선택하는 방법

  1. 편집기 상단 중앙(또는 좌측 상단)에 함수 선택 드롭다운이 있습니다.
    (스크린샷의 myFunction 부분)
  2. 그 드롭다운을 클릭해서 fetchRssToSheet를 선택하세요.
    (함수명이 보이지 않으면 코드에 function fetchRssToSheet()가 최상위(scope)로 존재하는지 확인하세요 — 다른 함수 안에 중첩되어 있으면 안 보입니다.)

4) 함수 실행 (수동)

  1. 함수 선택 후 상단 툴바의 ▶️ 실행(또는 Run) 버튼을 누릅니다.
  2. 최초 실행이면 권한 요청(승인) 팝업이 뜹니다. (다음 섹션 참조)

5) 권한(승인) 절차 — unverified 앱 경고 처리

  1. 계정 선택 창에서 사용할 계정 클릭
  2. “Google에서 확인하지 않은 앱” 경고가 나오면 하단의 고급 클릭
  3. (프로젝트명)으로 이동(안전하지 않음) 클릭
  4. 권한 허용(Allow) → 스크립트가 Spreadsheet, UrlFetchApp 등 필요한 권한을 요청하면 허용 클릭
  5. 허용 완료되면 자동으로 실행되거나 다시 ▶️ 실행 버튼을 누르시면 됩니다.

주의: 코드가 본인(또는 신뢰할 수 있는 출처)이면 허용해도 됩니다. 낯선 코드면 먼저 코드 내용을 확인하세요.


6) 실행 결과 확인

  • 실행이 성공하면 구글 시트(NewsFeed)에 새 행이 추가됩니다. 스프레드시트를 확인하세요.
  • 에러가 나면 편집기 우측 상단의 실행 로그(또는 Executions) 혹은 보기 → 로그 에서 상세 에러 메시지를 확인하세요.

7) 자주 발생하는 문제와 해결법

  • 함수 목록에 fetchRssToSheet가 안 보일 때
    • function fetchRssToSheet() 정의가 파일 안에 최상위로 있어야 합니다. 다른 함수 내부에 중첩되어 있으면 드롭다운에서 안 보입니다.
  • "시트가 없습니다" 오류
    • 스프레드시트에 시트 이름이 정확히 NewsFeed 인지 확인하세요(대소문자 포함).
  • "권한 없음" 관련 오류
    • 권한 승인을 완료했는지 확인하고, 승인한 계정으로 실행 중인지 확인하세요.
  • RSS 파싱 에러(XML 파싱 오류)
    • RSS URL이 실제 RSS를 반환하는지 브라우저에서 먼저 확인하세요. 일부 사이트는 RSS 접근을 막거나 HTML을 반환합니다.
  • 디버깅이 필요할 때
    • 코드에 Logger.log()를 넣고 실행 → 보기 → 로그로 출력 확인하세요.
function fetchRssToSheet() {
  try {
    Logger.log('fetch 시작');
    // 기존 코드...
  } catch (e) {
    Logger.log('에러: ' + e);
    throw e;
  }
}

8) 자동 실행(트리거) 설정

  1. 편집기 좌측 사이드바에서 시계 아이콘(트리거) 클릭
  2. 트리거 추가 버튼 클릭
  3. 설정:
    • 함수 선택: fetchRssToSheet
    • 이벤트 소스: 시간 기반(Time-driven)
    • 시간 기반 유형: 분 단위 타이머
    • 분 간격: 30분마다 (원하시면 5, 10, 15, 30 등)
  4. 저장 → 트리거가 주기적으로 자동 실행됩니다.

9) 빠른 체크리스트 (한눈에)

  • 스프레드시트에 NewsFeed 시트가 있는가?
  • 코드 저장했는가? (저장 아이콘 누름)
  • 편집기에서 함수 드롭다운에 fetchRssToSheet 선택했는가?
  • ▶️ 실행 클릭했고 권한 승인했는가?
  • 스프레드시트에 데이터가 들어왔는가?
  • 자동 실행은 트리거로 설정했는가?

문제 없이 실행했는데 시트에 데이터가 안 들어온다면, 보통은 아래 4가지 중 하나입니다.
(1) RSS가 제대로 안 읽혔거나
(2) XML 파싱이 실패했거나
(3) 시트 이름/위치 문제
(4) 코드 실행 중 오류 발생

아래 순서대로 점검해 보세요.


1️⃣ 실행 로그 확인

  1. Apps Script 편집기에서 상단 메뉴 → “보기 → 실행 로그” 클릭
    또는 좌측 메뉴의 “실행 기록(Executions)” 아이콘 클릭
  2. 최근 실행 항목을 열어 에러 메시지가 있는지 확인
    • 예: TypeError: Cannot read properties of undefined (reading 'getChildren')
    • 혹은 Exception: Request failed for ... returned code 403 등

👉 로그 메시지를 알려주시면 원인 파악이 빠릅니다.


2️⃣ RSS 주소 테스트

브라우저 주소창에 아래 RSS URL을 직접 열어보세요.

정상 RSS라면 XML 문서( <rss><channel>... ) 가 보여야 합니다.
❌ 404 Not Found 또는 웹페이지(HTML)가 뜬다면 그 RSS는 사용할 수 없습니다.


3️⃣ XML 파싱 방식 수정

CNBC처럼 구조가 다른 RSS는 document.getRootElement().getChild("channel") 로 접근하는 게 안전합니다.
코드를 약간 수정해보세요.

 

function fetchRssToSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");

  RSS_FEEDS.forEach(url => {
    try {
      const xml = UrlFetchApp.fetch(url).getContentText();
      const document = XmlService.parse(xml);
      const root = document.getRootElement();
      const channel = root.getChild("channel");
      const items = channel.getChildren("item");

      items.forEach(item => {
        const title = item.getChildText("title");
        const link = item.getChildText("link");
        const pubDate = item.getChildText("pubDate");
        const description = item.getChildText("description") || "";

        // 중복 체크
        const existing = sheet.getRange(2, 2, sheet.getLastRow(), 1).getValues().flat();
        if (existing.includes(title)) return;

        // 키워드 탐색
        const theme = KEYWORDS.find(k => title.includes(k) || description.includes(k)) || "기타";

        sheet.appendRow([pubDate, title, link, description, theme]);
      });
    } catch (err) {
      Logger.log(`❗ ${url} 처리 중 오류: ${err}`);
    }
  });
}
  • Logger.log로 어떤 URL에서 오류가 나는지 확인할 수 있습니다.

4️⃣ 시트 이름과 구조 점검

  • 시트 이름이 반드시 NewsFeed 이어야 합니다.
  • 첫 행에는 아무 데이터 없어도 되고, 2행부터 데이터가 쌓입니다.

5️⃣ 중복 검사 코드 제거 테스트

중복 검사 부분이 문제가 될 수도 있으니, 일단 주석 처리하고 테스트해보세요.

// const existing = sheet.getRange(2, 2, sheet.getLastRow(), 1).getValues().flat();
// if (existing.includes(title)) return;

 

실행시 에러 화면

에러 메시지 Exception: The number of rows in the range must be at least 1. 는 주로 빈 시트에서 getRange()로 잘못된 범위를 요청했을 때 발생합니다.

🔎 문제 원인

코드 25번째 줄:

const existing = sheet.getRange(2, 2, sheet.getLastRow(), 1).getValues().flat();
  • getLastRow() → 현재 시트에 데이터가 전혀 없으니 0을 반환
  • getRange(2, 2, 0, 1) → 행 수가 0이라 오류 발생

즉, 기존 뉴스가 없는데 2행부터 데이터를 읽으려 해서 생긴 문제입니다.


🟢 해결 방법 2가지

✅ 방법 1: 헤더 행을 먼저 추가하기

  1. 구글 시트 NewsFeed의 첫 번째 행에 헤더를 작성:
A1: 날짜      B1: 제목      C1: 링크      D1: 설명      E1: 테마

헤더 작성

   2. 시트가 비어 있지 않으니 getLastRow()가 1을 반환 → 오류 사라짐


✅ 방법 2: 코드에서 빈 시트 예외 처리

코드를 아래처럼 수정하세요:

// 중복 체크
let existing = [];
if (sheet.getLastRow() > 1) {   // 1행 이상 있을 때만 읽기
  existing = sheet.getRange(2, 2, sheet.getLastRow() - 1, 1).getValues().flat();
}
  • getLastRow() - 1로 실제 데이터만 읽고
  • 데이터가 없으면 existing은 빈 배열 유지

✍️ 수정된 전체 코드 (핵심 부분만)

function fetchRssToSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");

  RSS_FEEDS.forEach(url => {
    const xml = UrlFetchApp.fetch(url).getContentText();
    const document = XmlService.parse(xml);
    const items = document.getRootElement().getChildren("channel")[0].getChildren("item");

    items.forEach(item => {
      const title = item.getChildText("title");
      const link = item.getChildText("link");
      const pubDate = item.getChildText("pubDate");
      const description = item.getChildText("description");

      // 중복 체크
      let existing = [];
      if (sheet.getLastRow() > 1) {
        existing = sheet.getRange(2, 2, sheet.getLastRow() - 1, 1).getValues().flat();
      }
      if (existing.includes(title)) return;

      // 테마 키워드
      let theme = KEYWORDS.find(k => title.includes(k) || description.includes(k)) || "기타";

      sheet.appendRow([pubDate, title, link, description, theme]);
    });
  });
}

🚀 실행 순서

  1. 시트에 헤더행 작성 (방법 1) 또는 코드 수정 (방법 2)
  2. fetchRssToSheet() 실행
  3. 오류 없이 뉴스가 쌓이는지 확인

 

뉴스 제목과 설명을 자동으로 한글 번역해서 시트에 저장할 수 있습니다.

구글 앱스 스크립트에는 LanguageApp.translate() 라는 내장 번역 함수가 있어서 별도 API 키 없이 사용할 수 있어요.


✍️ 번역 추가 코드

아래처럼 제목(title)과 설명(description) 을 번역해서 제목(한글), 설명(한글) 열을 추가할 수 있습니다.

function fetchRssToSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");

  RSS_FEEDS.forEach(url => {
    const xml = UrlFetchApp.fetch(url).getContentText();
    const document = XmlService.parse(xml);
    const items = document.getRootElement().getChildren("channel")[0].getChildren("item");

    items.forEach(item => {
      const title = item.getChildText("title") || "";
      const link = item.getChildText("link") || "";
      const pubDate = item.getChildText("pubDate") || "";
      const description = item.getChildText("description") || "";

      // 중복 체크
      let existing = [];
      if (sheet.getLastRow() > 1) {
        existing = sheet.getRange(2, 2, sheet.getLastRow() - 1, 1).getValues().flat();
      }
      if (existing.includes(title)) return;

      // 🔹 한글 번역
      let titleKo = LanguageApp.translate(title, "", "ko");
      let descriptionKo = LanguageApp.translate(description, "", "ko");

      // 🔹 테마 분류
      let theme = KEYWORDS.find(k => title.includes(k) || description.includes(k)) || "기타";

      // 🔹 시트에 추가
      sheet.appendRow([pubDate, title, titleKo, link, description, descriptionKo, theme]);
    });
  });
}

📝 시트 헤더 예시

시트의 1행을 아래처럼 수정해 주세요.

A열 B열 C열 D열 E열 F열 G열
날짜 제목(원문) 제목(한글) 링크 설명(원문) 설명(한글) 테마

✅ 설명

  • LanguageApp.translate(text, sourceLang, targetLang)
    • sourceLang은 빈 문자열("") → 언어 자동 감지
    • targetLang은 "ko" → 한국어로 번역
  • 원문과 번역을 모두 저장하여 비교 가능
  • 무료로 사용 가능하지만 일일 호출 제한(일 약 50,000자) 있으니 뉴스가 매우 많으면 주의

🚀 실행 방법

  1. 기존 코드 대신 위의 코드로 교체
  2. 시트 헤더를 위 표대로 변경
  3. fetchRssToSheet() 실행 → 새로 수집된 뉴스부터 번역 포함 저장

🪄 1단계: 키워드를 테마별로 그룹화

현재는 이렇게 단일 배열일 가능성이 높습니다:

const KEYWORDS = ["반도체","AI","전기차","원전"];

이를 객체로 테마별 키워드 목록을 묶어보세요:

const THEME_KEYWORDS = {
  "AI/빅데이터": ["AI", "인공지능", "챗GPT", "딥러닝", "머신러닝", "OpenAI", "LLM"],
  "반도체/칩": ["반도체", "칩", "TSMC", "삼성전자", "DDR", "파운드리"],
  "전기차/배터리": ["전기차", "EV", "테슬라", "배터리", "CATL", "2차전지", "리튬"],
  "원자력/에너지": ["원전", "소형모듈원자로", "SMR", "재생에너지", "태양광", "풍력"],
  "바이오/헬스케어": ["바이오", "제약", "임상", "항암제", "백신"],
  "핀테크/블록체인": ["블록체인", "비트코인", "이더리움", "핀테크", "디파이", "NFT"],
  "우주/항공": ["위성", "스페이스X", "우주발사체", "항공기"],
  "게임/콘텐츠": ["게임", "메타버스", "콘텐츠", "웹툰", "OTT"],
};

🪄 2단계: 테마 매칭 로직 강화

현재 코드:

let theme = KEYWORDS.find(k => title.includes(k) || description.includes(k)) || "기타";

수정된 코드:

function getTheme(title, description) {
  const text = `${title} ${description}`.toLowerCase();
  for (const [theme, keywords] of Object.entries(THEME_KEYWORDS)) {
    for (const kw of keywords) {
      if (text.includes(kw.toLowerCase())) {
        return theme;
      }
    }
  }
  return "기타";
}

🪄 3단계: fetch 함수에 적용

function fetchRssToSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");

  RSS_FEEDS.forEach(url => {
    const xml = UrlFetchApp.fetch(url).getContentText();
    const document = XmlService.parse(xml);
    const items = document.getRootElement().getChildren("channel")[0].getChildren("item");

    items.forEach(item => {
      const title = item.getChildText("title") || "";
      const link = item.getChildText("link") || "";
      const pubDate = item.getChildText("pubDate") || "";
      const description = item.getChildText("description") || "";

      // 중복 제거
      let existing = [];
      if (sheet.getLastRow() > 1) {
        existing = sheet.getRange(2, 2, sheet.getLastRow() - 1, 1).getValues().flat();
      }
      if (existing.includes(title)) return;

      // 번역
      const titleKo = LanguageApp.translate(title, "", "ko");
      const descriptionKo = LanguageApp.translate(description, "", "ko");

      // 🔹 테마 분류
      const theme = getTheme(titleKo, descriptionKo);

      sheet.appendRow([pubDate, title, titleKo, link, description, descriptionKo, theme]);
    });
  });
}

📝 추가 팁

  1. 키워드 확장:
    테마별로 뉴스에 자주 등장하는 회사명, 약어 등을 계속 추가하세요.
  2. 우선순위:
    AI와 반도체 둘 다 포함된 뉴스는 AI/빅데이터를 우선시하는 등의 우선순위 로직도 넣을 수 있습니다.
  3. 정규식 활용:
    키워드가 ‘EV’, ‘E.V.’, ‘electric vehicle’처럼 변형될 수 있으니 정규식 매칭이 더 안정적입니다.
  4. GSheet에서 테마 키워드 관리:
    코드에 직접 키워드를 넣지 않고, 시트의 ThemeKeywords 탭에 작성 후 getRange() 로 불러오면 더 쉽게 관리할 수 있습니다.

테마 키워드를 코드 안이 아니라 스프레드시트에서 불러와 관리하면 훨씬 편해집니다.
아래 단계별로 설명드리겠습니다.


🗂️ 1. 시트 준비

  1. 기존 NewsFeed 시트 옆에 새 시트를 하나 더 만들고 이름을 ThemeKeywords 로 지정합니다.
  2. 아래처럼 입력합니다:
테마 키워드(쉼표로 구분)
AI/빅데이터 AI, 인공지능, 챗GPT, LLM, 딥러닝, 머신러닝, OpenAI, 데이터, 빅데이터
반도체/칩 반도체, 칩, DDR, 파운드리, TSMC, 삼성전자
전기차/배터리 전기차, EV, 테슬라, 2차전지, 배터리, 리튬, 전고체
원자력/에너지 원전, 소형모듈원자로, SMR, 태양광, 풍력, 재생에너지
바이오/헬스케어 바이오, 제약, 항암제, 백신, 임상, 유전자
핀테크/블록체인 블록체인, 비트코인, 이더리움, 핀테크, 디파이, NFT
우주/항공 위성, 스페이스X, 발사체, 항공기, 우주탐사
게임/콘텐츠 게임, 메타버스, 콘텐츠, 웹툰, OTT, AR, VR

💻 2. Apps Script 코드

// 🔹 시트에서 테마 키워드 로드
function loadThemeKeywords() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ThemeKeywords");
  const data = sheet.getDataRange().getValues();
  const themes = {};

  // 첫 행(헤더) 제외
  for (let i = 1; i < data.length; i++) {
    const theme = data[i][0];
    const keywords = (data[i][1] || "").split(",").map(k => k.trim());
    if (theme && keywords.length) {
      themes[theme] = keywords;
    }
  }
  return themes;
}

// 🔹 테마 분류
function getTheme(title, description, themeKeywords) {
  const text = `${title} ${description}`.toLowerCase();
  for (const [theme, keywords] of Object.entries(themeKeywords)) {
    for (const kw of keywords) {
      if (kw && text.includes(kw.toLowerCase())) {
        return theme;
      }
    }
  }
  return "기타";
}

// 🔹 RSS 가져오기
function fetchRssToSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");
  const themeKeywords = loadThemeKeywords();  // 시트에서 키워드 불러오기

  RSS_FEEDS.forEach(url => {
    const xml = UrlFetchApp.fetch(url).getContentText();
    const document = XmlService.parse(xml);
    const items = document.getRootElement().getChildren("channel")[0].getChildren("item");

    items.forEach(item => {
      const title = item.getChildText("title") || "";
      const link = item.getChildText("link") || "";
      const pubDate = item.getChildText("pubDate") || "";
      const description = item.getChildText("description") || "";

      // 중복 제거
      let existing = [];
      if (sheet.getLastRow() > 1) {
        existing = sheet.getRange(2, 2, sheet.getLastRow() - 1, 1).getValues().flat();
      }
      if (existing.includes(title)) return;

      // 번역
      const titleKo = LanguageApp.translate(title, "", "ko");
      const descriptionKo = LanguageApp.translate(description, "", "ko");

      // 🔹 테마 분류
      const theme = getTheme(titleKo, descriptionKo, themeKeywords);

      sheet.appendRow([pubDate, title, titleKo, link, description, descriptionKo, theme]);
    });
  });
}

⚙️ 3. 동작 방식

  • loadThemeKeywords()
    → ThemeKeywords 시트에서 테마와 키워드를 모두 읽어 객체 형태로 변환합니다.
    예:
{
  "AI/빅데이터": ["AI","인공지능","챗GPT",...],
  "전기차/배터리": ["전기차","EV","테슬라",...]
}
  • getTheme()
    → 뉴스의 제목+설명에 키워드가 있는지 순서대로 탐색해 테마 반환.
  • fetchRssToSheet()
    → RSS에서 뉴스 추출 → 번역 → 테마 분류 → NewsFeed 시트에 추가.

🚀 장점

✅ 코드 수정 없이 시트에서 키워드만 추가/삭제하면 바로 반영
✅ 여러 사람이 키워드를 관리하기 쉬움
✅ ‘기타’로 분류되는 기사 줄어듦


🔑 팁

  • 키워드 입력 시 반드시 **쉼표(,)**로 구분하세요.
  • 불필요한 공백은 trim() 함수로 제거되므로 크게 신경 쓰지 않아도 됩니다.
  • 키워드에 영어/한글 혼용 가능. 예: AI, 인공지능, LLM, 챗GPT
  • 테마 우선순위를 바꾸려면 시트의 행 순서를 조정하세요.

이 방식이면 테마 분류 정확도를 훨씬 쉽게 유지할 수 있습니다. 🚀

 

최종 소스 파일

    /***** 환경설정 *****/
  const RSS_FEEDS = [
    "https://www.cnbc.com/id/100003114/device/rss/rss.html", // CNBC
    "https://www.yna.co.kr/rss/economy.xml",                  // 연합뉴스 경제
    "https://www.edaily.co.kr/rss/stock.xml",                 // 이데일리 증권
  ];
 
 // 필터 키워드
  const THEME_KEYWORDS = {
  "AI/빅데이터": ["AI", "인공지능", "챗GPT", "딥러닝", "머신러닝", "OpenAI", "LLM"],
  "반도체/칩": ["반도체", "칩", "TSMC", "삼성전자", "DDR", "파운드리"],
  "전기차/배터리": ["전기차", "EV", "테슬라", "배터리", "CATL", "2차전지", "리튬"],
  "원자력/에너지": ["원전", "소형모듈원자로", "SMR", "재생에너지", "태양광", "풍력"],
  "바이오/헬스케어": ["바이오", "제약", "임상", "항암제", "백신"],
  "핀테크/블록체인": ["블록체인", "비트코인", "이더리움", "핀테크", "디파이", "NFT"],
  "우주/항공": ["위성", "스페이스X", "우주발사체", "항공기"],
  "게임/콘텐츠": ["게임", "메타버스", "콘텐츠", "웹툰", "OTT"],
};


function fetchRssToSheet() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("NewsFeed");
  const themeKeywords = loadThemeKeywords();  // 시트에서 키워드 불러오기


  RSS_FEEDS.forEach(url => {
    const xml = UrlFetchApp.fetch(url).getContentText();
    const document = XmlService.parse(xml);
    const items = document.getRootElement().getChildren("channel")[0].getChildren("item");

    items.forEach(item => {
      const title = item.getChildText("title");
      const link = item.getChildText("link");
      const pubDate = item.getChildText("pubDate");
      const description = item.getChildText("description");

      // 중복 체크
      let existing = [];
      if (sheet.getLastRow() > 1) {
        existing = sheet.getRange(2, 2, sheet.getLastRow() - 1, 1).getValues().flat();
      }
      if (existing.includes(title)) return;

            // 🔹 한글 번역
      let titleKo = LanguageApp.translate(title, "", "ko");
      let descriptionKo = LanguageApp.translate(description, "", "ko");

      // 🔹 테마 분류
      const theme = getTheme(titleKo, descriptionKo, themeKeywords);

      // 🔹 시트에 추가
      sheet.appendRow([pubDate, titleKo, link, descriptionKo, theme]);
      /** 
      // 테마 키워드
      let theme = KEYWORDS.find(k => title.includes(k) || description.includes(k)) || "기타";

      sheet.appendRow([pubDate, title, link, description, theme]);*/
    });
  });
}

  // 🔹 시트에서 테마 키워드 로드
function loadThemeKeywords() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ThemeKeywords");
  const data = sheet.getDataRange().getValues();
  const themes = {};

  // 첫 행(헤더) 제외
  for (let i = 1; i < data.length; i++) {
    const theme = data[i][0];
    const keywords = (data[i][1] || "").split(",").map(k => k.trim());
    if (theme && keywords.length) {
      themes[theme] = keywords;
    }
  }
  return themes;
}

// 🔹 테마 분류
function getTheme(title, description, themeKeywords) {
  const text = `${title} ${description}`.toLowerCase();
  for (const [theme, keywords] of Object.entries(themeKeywords)) {
    for (const kw of keywords) {
      if (kw && text.includes(kw.toLowerCase())) {
        return theme;
      }
    }
  }
  return "기타";


/** 
    // 🔹 테마 분류
  function getTheme(title, description) {
  const text = `${title} ${description}`.toLowerCase();
  for (const [theme, keywords] of Object.entries(THEME_KEYWORDS)) {
    for (const kw of keywords) {
      if (text.includes(kw.toLowerCase())) {
        return theme;
      }
    }
  }
  return "기타";
  } */
}
반응형