- Published on
DART API 완벽 가이드 2026: 전자공시 데이터 자동 수집부터 알림 시스템까지
- Authors

- Name
- 매크로 마스터

안녕하세요, 매크로마스터입니다.
이번 글은 DART API 사용법의 결정판으로 정리해봤습니다. 금융감독원이 운영하는 전자공시시스템(DART)은 상장기업의 모든 공시 데이터를 무료로 풀고 있지만, 막상 자동화하려고 들어가 보면 발급 절차부터 XML/JSON 혼재, 들쭉날쭉한 응답 포맷, 호출 제한까지 막히는 지점이 한두 군데가 아닙니다. 그동안 수십 건의 DART 크롤링과 전자공시 자동화 의뢰를 진행하면서 모아둔 실전 노하우를, 2026년 기준으로 한 번에 풀어두려고 합니다.
이 글을 따라 읽으시면 OPEN API 키 발급부터 공시 데이터 수집, 재무제표 크롤링, 키워드 기반 모니터링 알림까지 한 번에 정리됩니다. 더 짧은 데모성 사례는 기존 글 DART 전자공시 보고서 크롤링 / API 사용법에서 같이 보시면 좋습니다.
DART가 뭔지, 왜 자동화가 필요한가
DART(전자공시시스템, Data Analysis, Retrieval and Transfer System)는 금융감독원이 운영하는 공시 통합 플랫폼입니다. 상장사·비상장 외감법인이 의무적으로 올리는 정기보고서(사업보고서·반기보고서·분기보고서), 주요사항보고서(전환사채·신주인수권부사채·유상증자 등), 지분공시, 감사보고서가 전부 여기에 모입니다. 시장 분석가, 투자 운용사, 회계법인, 핀테크 스타트업이 모두 이 데이터를 원자료로 씁니다.
문제는 수작업으로는 도저히 못 따라간다는 점입니다. 코스피·코스닥·코넥스를 합치면 상장사만 2,600개가 넘고, 하루에 올라오는 공시는 평일 기준 평균 200건 안팎입니다. 분기보고서 시즌이 되면 하루 500건이 쏟아지기도 합니다. 사람이 일일이 새로고침해서 모니터링하는 건 불가능에 가깝습니다.
그래서 등장한 게 DART OPEN API입니다. 모든 공시 메타데이터와 정형화된 재무 정보를 API로 받아올 수 있어서, 키워드 알림이나 재무제표 데이터레이크 구축, 백테스팅용 데이터 수집 같은 전자공시 자동화 작업을 코드로 처리할 수 있습니다.
DART OPEN API 키 발급 방법
DART OPEN API를 쓰려면 별도 사이트인 opendart.fss.or.kr에서 인증키를 발급받아야 합니다. 본체 사이트(dart.fss.or.kr)와 다른 사이트라는 점이 헷갈리는 분이 많습니다.
1단계. opendart.fss.or.kr 접속 후 회원가입
OPEN DART 홈페이지 우측 상단의 "회원가입"을 클릭하고, 이메일 인증과 약관 동의를 거칩니다. 개인 계정으로도 발급이 가능하고, 법인 계정으로 받으면 호출 한도 협의가 가능합니다.
2단계. 인증키 신청
로그인 후 "인증키 신청/관리" 메뉴에 들어가서 사용 목적을 입력하고 신청합니다. 사용 목적은 "재무 데이터 분석" "공시 모니터링" 정도로 적으면 보통 즉시 발급됩니다. 보류된 경험은 거의 없습니다.
3단계. 발급된 키 확인
마이페이지의 "오픈API 이용현황"에서 40자리 인증키를 확인할 수 있습니다. 이 키가 모든 요청의 crtfc_key 파라미터로 들어갑니다. 절대 깃허브 공개 저장소에 올리지 마세요. 환경변수로 빼두는 게 안전합니다.
4단계. 호출 한도 확인
기본 호출 한도는 인증키당 하루 20,000건입니다. 공시 모니터링 정도면 충분하지만, 전 종목 재무제표를 크롤링하는 대규모 작업이라면 금세 한도에 닿습니다. 한도 상향은 사유서 작성 후 OPEN DART 운영팀에 메일로 신청해야 하고, 통상 1~2주가 걸립니다. 실무 프로젝트라면 이 시간까지 계산에 넣어두셔야 합니다.
자주 쓰는 DART API 엔드포인트
DART OPEN API는 수십 개의 엔드포인트가 있지만, 실무에서 90%는 아래 일곱 개로 끝납니다. 응답 형식, 호출 비용, 활용 시점을 같이 정리했습니다.
1. 공시검색 (/api/list.json)
기간·회사·공시유형으로 공시 목록을 가져옵니다. 거의 모든 크롤링 작업의 시작점입니다. 페이지네이션 파라미터(page_no, page_count)가 있고 한 페이지 최대 100건, 최대 페이지 100까지라 한 번에 가져올 수 있는 게 1만 건이라는 제약이 있습니다. 기간을 잘게 쪼개야 하는 이유가 여기 있습니다.
2. 기업개황 (/api/company.json)
법인등록번호, 대표자명, 설립일, 결산월, 업종 같은 회사 기본 정보를 받습니다. 다만 회사 식별자로 종목코드가 아니라 DART 고유번호(corp_code, 8자리)를 써야 합니다.
3. 고유번호 (/api/corpCode.xml)
전 종목의 corp_code ↔ 종목코드 ↔ 회사명 매핑 테이블을 ZIP으로 한 번에 받습니다. 처음 한 번만 받아두고 로컬 SQLite나 파케이 파일로 캐싱해두는 게 정석입니다.
4. 단일회사 주요계정 (/api/fnlttSinglAcnt.json)
특정 회사의 한 사업연도 주요 재무 항목(매출, 영업이익, 순이익, 자산, 부채 등)을 받습니다. 빠르고 가볍지만 항목이 제한적입니다.
5. 단일회사 전체 재무제표 (/api/fnlttSinglAcntAll.json)
위와 비슷한데 전체 재무제표 라인을 다 줍니다. 연결재무제표, 별도재무제표 구분 파라미터(fs_div)가 중요합니다. 백테스팅용 데이터는 거의 다 여기서 나옵니다.
6. 다중회사 주요계정 (/api/fnlttMultiAcnt.json)
여러 회사의 주요계정을 한 번에 비교할 때 씁니다. 동종업계 비교 분석에 자주 사용합니다.
7. 공시서류원본파일 (/api/document.xml)
rcept_no(접수번호)로 공시 원본 파일을 받습니다. ZIP에 XML이 들어 있고, 그 안에 본문 텍스트와 표가 다 들어 있습니다. 사업보고서 텍스트 마이닝의 출발점입니다.
이 외에 "주식의 총수 현황" "최대주주 현황" "임원·직원 현황" 같은 사업보고서 주요정보 API도 21개가 더 있는데, 필요할 때 골라 쓰면 됩니다.
실전 Python 예제 1: 특정 기업 공시 목록 가져오기
이제 코드입니다. requests 라이브러리 하나면 충분하고, 응답은 대부분 JSON으로 받을 수 있어서 처리가 깔끔합니다.
import os
import requests
import pandas as pd
from datetime import datetime
API_KEY = os.environ["DART_API_KEY"]
BASE_URL = "https://opendart.fss.or.kr/api"
def get_disclosures(corp_code: str, start: str, end: str) -> pd.DataFrame:
"""특정 회사의 공시 목록을 기간으로 가져옴.
start, end는 'YYYYMMDD' 형식. 페이지네이션 자동 처리."""
results = []
page_no = 1
while True:
params = {
"crtfc_key": API_KEY,
"corp_code": corp_code,
"bgn_de": start,
"end_de": end,
"page_no": page_no,
"page_count": 100, # 한 페이지 최대치
}
r = requests.get(f"{BASE_URL}/list.json", params=params, timeout=10)
data = r.json()
if data.get("status") != "000": # '000'이 정상 응답
break
results.extend(data["list"])
if page_no >= data["total_page"]:
break
page_no += 1
return pd.DataFrame(results)
# 삼성전자 corp_code: 00126380
df = get_disclosures("00126380", "20250101", "20251231")
print(df[["report_nm", "rcept_dt", "rcept_no"]].head())
여기서 주의할 점은 status 코드 분기입니다. DART API는 데이터가 없을 때도 200 OK로 응답하면서 status만 013(조회된 데이터가 없습니다)으로 떨어집니다. r.status_code만 보면 에러를 놓칩니다. 반드시 응답 본문의 status를 확인하세요. 이걸 모르고 짠 코드는 빈 데이터가 들어와도 조용히 통과해버려서, 모니터링 시스템이 한참 뒤에 문제를 들킵니다.
실전 Python 예제 2: 분기 재무제표 자동 다운로드
전 종목의 분기 재무제표를 한 번에 받는 작업은 가장 자주 의뢰받는 일 중 하나입니다. 핵심은 reprt_code(보고서 코드)와 fs_div(재무제표 구분)의 조합입니다.
import time
REPORT_CODES = {
"1Q": "11013", # 1분기보고서
"2Q": "11012", # 반기보고서
"3Q": "11014", # 3분기보고서
"FY": "11011", # 사업보고서 (연간)
}
def get_full_fs(corp_code: str, year: int, quarter: str,
fs_div: str = "CFS") -> pd.DataFrame:
"""전체 재무제표 라인 가져오기. fs_div: CFS(연결) / OFS(별도)."""
params = {
"crtfc_key": API_KEY,
"corp_code": corp_code,
"bsns_year": str(year),
"reprt_code": REPORT_CODES[quarter],
"fs_div": fs_div,
}
r = requests.get(f"{BASE_URL}/fnlttSinglAcntAll.json",
params=params, timeout=10)
data = r.json()
if data.get("status") != "000":
return pd.DataFrame()
return pd.DataFrame(data["list"])
# 여러 회사 순회 (rate limit 고려해 sleep)
corp_codes = ["00126380", "00164779", "00401731"] # 삼성전자, SK하이닉스, 현대차
all_fs = []
for code in corp_codes:
df = get_full_fs(code, 2025, "3Q", fs_div="CFS")
df["corp_code"] = code
all_fs.append(df)
time.sleep(0.1) # 초당 10건 정도가 안전
combined = pd.concat(all_fs, ignore_index=True)
여기서 두 가지 함정이 있습니다.
첫째, 연결 vs 별도 구분입니다. 지주회사나 은행 같은 곳은 연결재무제표(CFS)가 의미 있지만, 일부 비금융 단일법인은 별도재무제표(OFS)만 공시합니다. CFS로 요청했는데 데이터가 비어 있으면 자동으로 OFS로 재시도하는 로직을 넣어야 빈 셀 없이 모입니다.
둘째, 계정과목 명칭이 회사마다 다릅니다. 어떤 회사는 "매출액"이고 어떤 회사는 "영업수익"입니다. 은행은 "이자수익"이 매출에 해당합니다. 이걸 모르고 account_nm == "매출액"으로 필터링하면 절반은 빈 값으로 빠집니다. 표준계정코드(account_id, XBRL 분류체계)로 매핑하는 게 정석인데, 매핑 테이블을 따로 관리해야 해서 신경 쓸 일이 많습니다.
실전 Python 예제 3: 키워드 기반 공시 모니터링 알림
투자사나 IR 담당자가 가장 좋아하는 자동화입니다. "전환사채" "유상증자" "최대주주변경" 같은 키워드가 포함된 공시가 올라오면 즉시 텔레그램이나 슬랙으로 알림을 보내는 시스템입니다.
import sqlite3
import requests
KEYWORDS = ["전환사채", "유상증자", "최대주주변경", "주식분할"]
TELEGRAM_TOKEN = os.environ["TG_TOKEN"]
CHAT_ID = os.environ["TG_CHAT_ID"]
def fetch_today_disclosures(date: str) -> list[dict]:
"""당일 전체 공시 가져오기 (corp_code 생략하면 전 종목)."""
params = {
"crtfc_key": API_KEY,
"bgn_de": date, "end_de": date,
"page_no": 1, "page_count": 100,
}
items = []
while True:
r = requests.get(f"{BASE_URL}/list.json", params=params, timeout=10).json()
if r.get("status") != "000":
break
items.extend(r["list"])
if params["page_no"] >= r["total_page"]:
break
params["page_no"] += 1
return items
def alert(item: dict, keyword: str):
url = f"https://dart.fss.or.kr/dsaf001/main.do?rcpNo={item['rcept_no']}"
msg = f"[{keyword}] {item['corp_name']}\n{item['report_nm']}\n{url}"
requests.post(
f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
data={"chat_id": CHAT_ID, "text": msg},
)
def run_once(date: str, db_path: str = "seen.db"):
conn = sqlite3.connect(db_path)
conn.execute("CREATE TABLE IF NOT EXISTS seen(rcept_no TEXT PRIMARY KEY)")
for it in fetch_today_disclosures(date):
if conn.execute("SELECT 1 FROM seen WHERE rcept_no=?",
(it["rcept_no"],)).fetchone():
continue
for kw in KEYWORDS:
if kw in it["report_nm"]:
alert(it, kw)
break
conn.execute("INSERT INTO seen VALUES(?)", (it["rcept_no"],))
conn.commit()
cron이나 systemd timer로 1분 간격 호출, 또는 GitHub Actions의 schedule 트리거로 5분 간격 호출하면 됩니다. SQLite에 접수번호를 저장해두는 이유는 중복 알림 방지입니다. DART는 같은 보고서를 정정공시로 다시 올리는 경우가 많아서, 단순히 시간으로만 필터링하면 같은 알림이 여러 번 갑니다.
여기서 더 들어가면 키워드별 채널 분리, 회사 화이트리스트, 자연어 처리로 본문 요약, 정정공시 비교까지 확장됩니다. 시리즈로 묶어두면 사실상 미니 IR 모니터링 서비스가 됩니다. 텔레그램 봇 구현 자체가 처음이라면 텔레그램 봇 만들기 글을 같이 보시면 토큰 발급부터 따라하실 수 있습니다.
실무에서 마주치는 DART API의 함정
데모 코드는 위에서 보신 대로 깔끔하지만, 실제 프로덕션에서는 사방에서 예외가 터집니다. 의뢰 받은 프로젝트들에서 가장 자주 만난 함정만 정리했습니다.
호출 한도 (Rate Limit)
하루 20,000건이 넉넉해 보여도, 전 종목 4분기 재무제표를 한 번 풀로 돌리면 2,600개 회사 × 2개 재무제표 구분(CFS/OFS) × 보고서 5종 = 26,000건이 그냥 나옵니다. 한도에 닿으면 020(사용한도 초과) 상태코드로 응답이 떨어지면서 그날 더 이상 호출이 안 됩니다. 한도 상향 신청과 함께 캐싱 전략이 필수입니다. 이미 받은 보고서는 다시 받지 않도록 접수번호 기반 디둡을 반드시 넣으세요.
XML vs JSON 혼재
공시 메타데이터와 정형화된 재무 정보는 JSON으로 깔끔하게 오지만, 원본 보고서는 ZIP 안의 XML로 옵니다. 그것도 표 안에 표가 들어간 HTML 비슷한 XML이라 파싱이 까다롭습니다. 같은 "전환사채 발행 결정"이라는 보고서 안에서도 회사마다 표 구조가 미세하게 다릅니다. 기존 글에서 보여드린 정규식 처리는 이런 이유 때문입니다.
계정과목 표준화 부재
위에서도 짧게 언급했지만, 이게 재무제표 크롤링의 가장 큰 골칫거리입니다. K-IFRS 표준계정코드는 있지만, 실제 공시에서는 회사가 임의로 풀어쓰는 경우가 많습니다. "유동성장기차입금"과 "유동성 장기 차입금"이 다른 계정으로 인식되기도 합니다. 결국 회사별, 업종별 매핑 사전을 따로 관리해야 합니다.
정정공시 처리
같은 사업보고서가 1차, 2차, 3차로 정정되어 올라옵니다. 최신본만 보여줘야 하는데, API 응답에서는 정정 여부를 직접적으로 알려주지 않고 rm(비고) 필드의 텍스트나 보고서명에 "[기재정정]" 같은 표기로 들어옵니다. 텍스트 파싱이 들어가야 합니다.
휴일/장 시작 전 트래픽 폭증
분기 마감 직후나 사업보고서 마감일(통상 3월 말, 8월 말, 11월 중순)은 트래픽이 몰려서 API 응답이 느려지거나 일시 장애가 납니다. 재시도 로직과 지수 백오프(exponential backoff)가 필수입니다.
HTTPS 인증서 갱신 지연
가끔 OPEN DART 서버 인증서 갱신이 누락되어 SSLError가 터지는 경우가 있습니다. verify=False로 우회하는 코드를 인터넷에서 자주 보지만 보안상 위험합니다. 차라리 certifi를 최신으로 유지하거나, 임시 우회 시에도 화이트리스트 도메인에만 적용하세요.
이런 함정들은 하나하나는 사소해 보이지만, 같이 부딪히면 "왜 데이터가 부분만 들어오지?" "왜 똑같은 공시가 세 번 오지?" 같은 디버깅에 며칠씩 빠지게 만듭니다. 실제로 의뢰주들이 "직접 해보다가 포기하고 연락드렸다"고 말씀하시는 지점이 거의 다 여기입니다.
직접 구축 vs 외주 비교
DART 자동화 시스템을 사내에서 직접 구축할지, 외주를 줄지는 결국 시간과 유지보수 비용의 문제입니다. 솔직하게 정리합니다.
직접 구축의 현실 비용
- 초기 학습 곡선: API 스펙 파악과 인증키 발급, 첫 동작 코드까지 1주차
- MVP 수준 (공시 목록 + 알림): 빠르면 2주, 보통 1개월
- 데이터 정합성 확보 (계정 매핑, 정정공시 처리, 빈 응답 대응): 추가 1~2개월
- 운영 인프라 (스케줄러, DB, 모니터링, 로그): 1주 ~ 2주
- 장기 유지보수: API 변경 대응, 한도 관리, 인증서 이슈 대응에 월 5~10시간
이걸 사내 개발자 한 명이 부업처럼 진행하면 보통 34개월이 걸리고, 그동안 본업 일정은 밀립니다. 사람 인건비를 시간당 5만원으로만 잡아도 200300만원 어치 시간이 들어갑니다.
외주의 비용 구조
저희 매크로마스터의 경우, 키워드 알림 + 재무제표 수집 + 슬랙/텔레그램 통합 정도의 표준 패키지면 2~3주 안에 납품 가능합니다. 코드 소유권 전체 이전, 환경 세팅 가이드, 30일 무상 유지보수가 기본입니다. 비용 측면에서도 인하우스로 짜는 것보다 거의 항상 저렴합니다. 같은 일을 여러 번 해본 사람이 짧게 끝낸다는 단순한 이유입니다.
외주가 유리한 케이스
- 본업이 코딩이 아닌 분 (회계사, 애널리스트, IR, 운용역)
- 사내에 개발자는 있지만 본업이 따로 있어서 부업으로 짜기 어려운 경우
- 일회성 데이터 수집이 아니라 장기적으로 운영해야 하는 시스템
- 정확도가 중요한 경우 (재무제표 정합성, 알림 누락 0건 요구)
직접 구축이 나은 케이스
- 학습 자체가 목적인 경우 (개발자 스터디, 사이드 프로젝트)
- 단발성 1회 추출이고 정확도 요구가 낮은 경우
- 사내에 데이터 엔지니어 풀타임 1명 이상이 이미 있는 경우
실제 진행해 드린 작업들이 어떤 형태로 나오는지는 포트폴리오 페이지에서 확인하실 수 있습니다.
자주 묻는 질문
Q1. DART OPEN API는 유료인가요?
무료입니다. 회원가입 후 인증키만 발급받으면 됩니다. 다만 기본 호출 한도(20,000건/일) 상향은 별도 신청해야 하고, 운영팀 심사를 거칩니다.
Q2. 종목코드로 바로 조회할 수 없나요?
직접은 안 됩니다. DART 고유번호(corp_code, 8자리)로 조회해야 합니다. /api/corpCode.xml에서 ZIP을 한 번 받아두면 종목코드 ↔ corp_code ↔ 회사명 매핑 테이블이 들어있으니 로컬에 캐싱해두세요.
Q3. 비상장사 재무제표도 받을 수 있나요?
외감법인은 가능합니다. 외부감사 대상 비상장 법인의 감사보고서가 DART에 올라오므로 동일한 API로 받을 수 있습니다. 다만 공시 빈도가 연 1회뿐이라 분기 데이터는 없습니다.
Q4. 사업보고서 본문 텍스트(예: 사업의 내용)는 어떻게 가져오나요?
/api/document.xml로 보고서 원본 ZIP을 받은 뒤, 안의 XML을 파싱해야 합니다. 사업의 내용, 임원 현황, 주주 현황 같은 정형 섹션은 별도의 사업보고서 주요정보 API(21종)로 받는 게 훨씬 깔끔합니다.
Q5. 실시간 공시 알림은 얼마나 빠르게 가능한가요?
DART OPEN API는 실시간 푸시가 아니라 폴링(polling) 방식입니다. 1분 간격으로 /api/list.json을 호출해서 신규 공시를 잡아내는 게 보통입니다. 공시 게재 시점 기준 통상 1~3분 안에 알림 발송이 가능합니다. 더 빠르게는 DART 본체 사이트의 RSS 피드를 같이 쓰기도 합니다.
Q6. Pandas DataReader나 OpenDartReader 같은 라이브러리를 쓰면 안 되나요?
써도 됩니다. 빠른 프로토타이핑에는 OpenDartReader가 유용합니다. 다만 라이브러리가 모든 엔드포인트를 다 감싸지 않고, 응답 정규화 과정에서 일부 필드가 잘리는 경우가 있습니다. 프로덕션 수준의 정확도가 필요하면 결국 requests로 직접 호출하는 게 정석입니다.
Q7. DART와 다른 공공 데이터 API를 연계하려면?
가능합니다. 예를 들어 국가법령정보 OPEN API와 묶어서 "특정 법령 개정 → 영향받는 업종 공시 추적"같은 워크플로를 만들 수 있습니다. 공공·금융 데이터 연계 프로젝트는 의외로 의뢰가 자주 들어오는 영역입니다.
마무리: 데이터는 자동으로, 분석은 사람이
DART API 자체는 잘 만들어져 있습니다. 다만 "잘 만든 API"와 "실무에서 신뢰할 수 있는 데이터 파이프라인"은 거리가 멉니다. 인증키 한 줄 받고 첫 호출 성공하는 데까지는 30분이지만, 그 데이터를 매일 안정적으로 받아서 알림이 누락 없이 가고 정정공시까지 따라 잡는 시스템을 만드는 데는 보통 한 분기가 걸립니다.
본인의 본업이 데이터 엔지니어링이 아니라면, 그 한 분기는 분석과 의사결정에 쓰는 편이 훨씬 남습니다. DART 크롤링, 전자공시 자동화, 재무제표 데이터 파이프라인 구축 의뢰는 서비스 페이지에서 상세 견적을 확인하실 수 있고, 카카오톡으로 바로 상담도 가능합니다.
현금영수증 또는 세금계산서 발행 가능합니다.
