1. 전체 전략 개요
- 뉴스 수집 자동화
- 해외/국내 주요 언론사에서 경제·산업·정책 관련 기사 자동 수집
- 핵심 키워드 추출 & 알림
- 기사에서 새로 부각되는 키워드(예: AI반도체, 수소, 전기차 보조금) 자동 추출
- 테마 연결
- 키워드 → 관련 산업 → 관련 국내 상장사 매핑
- 실시간 모니터링 & 필터링
- 종목별 주가/거래량 급등락과 뉴스 발생 시점 비교
- 테마 랭킹 & 포트폴리오 업데이트
- 언급량·거래량·기관/외국인 수급 지표를 합쳐 점수화
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 피드를 제공합니다.
- 예) https://www.cnbc.com/id/100003114/device/rss/rss.html
- RSS → Feedly(앱) or Feedwind + Tistory로 자동 포스팅 가능
- 구글시트 + Apps Script로도 RSS 데이터를 가져와 저장 가능
(2) 뉴스 API
- NewsAPI.org(영/한 기사 수집 가능)
- 네이버 뉴스 API(네이버 개발자센터)
- 키워드 필터: "AI" OR "전기차" OR "수소" 등 설정
(3) 웹 스크래핑
- 파이썬 BeautifulSoup + requests 사용
- 특정 언론사의 산업/증권 섹션 크롤링
- 하루 1~2회 크론탭(스케줄러)로 자동 실행
4. 키워드 → 테마 매핑
- 텍스트 분석으로 키워드 추출
- KeyBERT / KoNLPy / Keyphrase Extraction
- 새로 부각되는 키워드(예: “AI 반도체”, “차세대 배터리”, “폐배터리 재활용”)
- 테마 사전 구축
- 예) 수소 → 효성첨단소재, 일진하이솔루스, 풍국주정
- 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. 개요
목표:
- RSS 뉴스 → 구글 시트 자동 저장
- 시트에서 특정 키워드(테마) 필터링
- 필터된 뉴스 → 티스토리 자동 포스팅
- 동시에 텔레그램으로 푸시 알림 전송
🟢 2. 준비물
- 구글 계정 + 구글 시트
- 티스토리 계정 (+ 티스토리 Open API)
- 텔레그램 Bot (@BotFather로 토큰 발급)
- 주요 RSS 피드 URL (예: CNBC, 연합뉴스, 이데일리 등)
🟢 3. 구글 시트 설정
- 새 시트를 만듭니다.
- 시트 이름을 NewsFeed로 변경
- A열: 날짜, B열: 제목, C열: 링크, D열: 요약, E열: 테마(자동 분류용)
🟢 4. 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 티스토리 포스팅
- Tistory Open API에서 애플리케이션 등록
- OAuth 인증 후 access_token 확보
- 블로그 이름(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 텔레그램 알림
- BotFather → /newbot으로 Bot 생성 → API 토큰 복사
- 텔레그램에서 @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 트리거 설정
- Apps Script → 시계 아이콘(트리거) 클릭
- fetchRssToSheet → 30분마다 실행
- postToTistory → 하루 1회 실행
- sendTelegramAlert → 매 실행 시마다 호출
🟢 5. 워크플로우
- RSS 뉴스 → 구글 시트 저장
- 키워드 필터로 테마 분류
- 최신 뉴스 5개 티스토리 자동 포스팅
- 새 뉴스가 들어올 때마다 텔레그램 푸시
🟢 6. 확장 아이디어
- Sentiment 분석으로 긍/부정 뉴스 필터링
- 거래량·주가 API와 연동 → 뉴스+주가 급등 종목만 알림
- 번역 API(예: 구글 번역) 연결 → 해외뉴스 한글 자동 변환
- 티스토리 SEO용 태그 자동 추가 (tag 파라미터 활용)
지금 보신 “Google에서 확인하지 않은 앱” 경고는 정상적인 과정입니다.
Apps Script로 처음 URLFetch, 시트 접근 등을 하면 구글이 권한 검토(승인) 를 요구합니다.
→ 개발자가 본인일 때는 무시하고 계속 진행해도 됩니다.
아래 순서대로 하면 됩니다.
🔑 1. 경고창 해결 방법
- 실행 클릭 → “승인 필요” → 권한 검토 클릭
- “Google에서 확인하지 않은 앱” 경고창이 뜨면
- 하단의 ‘고급’ 링크를 클릭
- ‘프로젝트명(NewsFeed)으로 이동(안전하지 않음)’ 클릭
- 구글 계정을 선택하고
- “이 앱이 Google 계정의 데이터에 액세스하도록 허용” → 허용
- 완료 후 다시 실행 → 이번엔 오류 없이 동작할 겁니다.
⚠️ 주의: 이 앱은 본인이 작성한 코드이므로 안심하고 승인하면 됩니다.
외부에서 받은 코드라면 반드시 코드 내용을 확인한 뒤 승인해야 합니다.
🟢 2. 권한 요청 이유
- SpreadsheetApp → 시트 읽기/쓰기 권한
- UrlFetchApp → 외부 RSS 가져오기
- XmlService → XML 파싱
모두 뉴스 수집 및 저장에 필요한 정상 권한입니다.
🟢 3. 실행 순서 체크
- 시트에 NewsFeed 시트가 있어야 함
- fetchRssToSheet() 함수를 선택 후 실행
- 승인 완료 후 실행하면 시트에 RSS 뉴스가 자동으로 추가됩니다.
🟢 4. 추가 팁
- 첫 실행 시 1~2개 RSS만 넣고 테스트하면 오류 찾기 쉽습니다.
- 권한 승인 후 트리거 설정(예: 30분마다 실행)까지 해두면 자동화 완성.
👉 따라서 “고급 → (프로젝트명)으로 이동 → 허용” 경로로 승인하시면 됩니다.
실행 후 시트에 데이터가 들어오면 성공입니다.
1) Apps Script 편집기 열기 (만약 아직 안 하셨다면)
- 구글 스프레드시트에서 상단 메뉴 → 확장 프로그램 → Apps Script 클릭
(또는 https://script.google.com 에서 프로젝트 열기)
2) 저장(저장 아이콘 누르기)
- 코드 붙여넣고 나서 저장 아이콘(디스크 모양) 또는 Ctrl+S로 저장하세요.
저장하지 않으면 최신 코드가 실행되지 않습니다.
3) 실행할 함수 선택하는 방법
- 편집기 상단 중앙(또는 좌측 상단)에 함수 선택 드롭다운이 있습니다.
(스크린샷의 myFunction 부분) - 그 드롭다운을 클릭해서 fetchRssToSheet를 선택하세요.
(함수명이 보이지 않으면 코드에 function fetchRssToSheet()가 최상위(scope)로 존재하는지 확인하세요 — 다른 함수 안에 중첩되어 있으면 안 보입니다.)
4) 함수 실행 (수동)
- 함수 선택 후 상단 툴바의 ▶️ 실행(또는 Run) 버튼을 누릅니다.
- 최초 실행이면 권한 요청(승인) 팝업이 뜹니다. (다음 섹션 참조)
5) 권한(승인) 절차 — unverified 앱 경고 처리
- 계정 선택 창에서 사용할 계정 클릭
- “Google에서 확인하지 않은 앱” 경고가 나오면 하단의 고급 클릭
- (프로젝트명)으로 이동(안전하지 않음) 클릭
- 권한 허용(Allow) → 스크립트가 Spreadsheet, UrlFetchApp 등 필요한 권한을 요청하면 허용 클릭
- 허용 완료되면 자동으로 실행되거나 다시 ▶️ 실행 버튼을 누르시면 됩니다.
주의: 코드가 본인(또는 신뢰할 수 있는 출처)이면 허용해도 됩니다. 낯선 코드면 먼저 코드 내용을 확인하세요.
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) 자동 실행(트리거) 설정
- 편집기 좌측 사이드바에서 시계 아이콘(트리거) 클릭
- 트리거 추가 버튼 클릭
- 설정:
- 함수 선택: fetchRssToSheet
- 이벤트 소스: 시간 기반(Time-driven)
- 시간 기반 유형: 분 단위 타이머
- 분 간격: 30분마다 (원하시면 5, 10, 15, 30 등)
- 저장 → 트리거가 주기적으로 자동 실행됩니다.
9) 빠른 체크리스트 (한눈에)
- 스프레드시트에 NewsFeed 시트가 있는가?
- 코드 저장했는가? (저장 아이콘 누름)
- 편집기에서 함수 드롭다운에 fetchRssToSheet 선택했는가?
- ▶️ 실행 클릭했고 권한 승인했는가?
- 스프레드시트에 데이터가 들어왔는가?
- 자동 실행은 트리거로 설정했는가?
문제 없이 실행했는데 시트에 데이터가 안 들어온다면, 보통은 아래 4가지 중 하나입니다.
(1) RSS가 제대로 안 읽혔거나
(2) XML 파싱이 실패했거나
(3) 시트 이름/위치 문제
(4) 코드 실행 중 오류 발생
아래 순서대로 점검해 보세요.
1️⃣ 실행 로그 확인
- Apps Script 편집기에서 상단 메뉴 → “보기 → 실행 로그” 클릭
또는 좌측 메뉴의 “실행 기록(Executions)” 아이콘 클릭 - 최근 실행 항목을 열어 에러 메시지가 있는지 확인
- 예: TypeError: Cannot read properties of undefined (reading 'getChildren')
- 혹은 Exception: Request failed for ... returned code 403 등
👉 로그 메시지를 알려주시면 원인 파악이 빠릅니다.
2️⃣ RSS 주소 테스트
브라우저 주소창에 아래 RSS URL을 직접 열어보세요.
- https://www.cnbc.com/id/100003114/device/rss/rss.html
- https://www.yna.co.kr/rss/economy.xml
- https://www.edaily.co.kr/rss/stock.xml
정상 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: 헤더 행을 먼저 추가하기
- 구글 시트 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) 또는 코드 수정 (방법 2)
- fetchRssToSheet() 실행
- 오류 없이 뉴스가 쌓이는지 확인
뉴스 제목과 설명을 자동으로 한글 번역해서 시트에 저장할 수 있습니다.
구글 앱스 스크립트에는 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자) 있으니 뉴스가 매우 많으면 주의
🚀 실행 방법
- 기존 코드 대신 위의 코드로 교체
- 시트 헤더를 위 표대로 변경
- 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]);
});
});
}
📝 추가 팁
- 키워드 확장:
테마별로 뉴스에 자주 등장하는 회사명, 약어 등을 계속 추가하세요. - 우선순위:
AI와 반도체 둘 다 포함된 뉴스는 AI/빅데이터를 우선시하는 등의 우선순위 로직도 넣을 수 있습니다. - 정규식 활용:
키워드가 ‘EV’, ‘E.V.’, ‘electric vehicle’처럼 변형될 수 있으니 정규식 매칭이 더 안정적입니다. - GSheet에서 테마 키워드 관리:
코드에 직접 키워드를 넣지 않고, 시트의 ThemeKeywords 탭에 작성 후 getRange() 로 불러오면 더 쉽게 관리할 수 있습니다.
테마 키워드를 코드 안이 아니라 스프레드시트에서 불러와 관리하면 훨씬 편해집니다.
아래 단계별로 설명드리겠습니다.
🗂️ 1. 시트 준비
- 기존 NewsFeed 시트 옆에 새 시트를 하나 더 만들고 이름을 ThemeKeywords 로 지정합니다.
- 아래처럼 입력합니다:
테마 | 키워드(쉼표로 구분) |
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 "기타";
} */
}