2026년 11월 13일 금요일

반도체 주식을 보기 전에 그려 두는 지도 — 설계, 파운드리, 메모리, 장비, 소재, 후공정

"반도체주"라고 한 묶음으로 부르지만, 그 안의 회사들은 하는 일도 돈 버는 방식도 전혀 다르다. 같은 뉴스에 어떤 회사는 오르고 어떤 회사는 내리는 이유가 여기에 있다. 종목을 들여다보기 전에 머릿속에 그려 두면 좋은 지도를 정리한다.

여섯 구역

  • 설계(팹리스). 칩을 설계만 하고 생산은 맡긴다. 공장이 없어 고정비가 작고, 제품이 잘 팔리면 이익률이 높다. AI 가속기, 모바일 프로세서, 통신 칩 회사들이 여기에 속한다.
  • 위탁 생산(파운드리). 남의 설계를 받아 만들어 준다. 최첨단 공정은 극소수 회사만 할 수 있고, 공장 하나에 수십조 원이 든다. 가동률이 실적을 좌우한다.
  • 메모리. D램과 낸드를 설계부터 생산까지 직접 한다. 제품이 규격화돼 있어 가격이 수요와 공급에 따라 크게 출렁인다. 그래서 실적의 진폭이 가장 크다.
  • 장비. 공장에 들어가는 기계를 만든다. 고객사의 설비 투자 계획이 곧 이 회사들의 주문이다. 칩 수요보다 한두 분기 먼저 움직이는 경향이 있다.
  • 소재·부품. 웨이퍼, 가스, 화학 약품, 부품을 공급한다. 공장이 돌아가는 동안 계속 팔리므로 장비보다 실적이 덜 출렁인다.
  • 후공정(패키징·테스트). 만든 칩을 자르고, 쌓고, 연결하고, 검사한다. 예전에는 부가가치가 낮은 단계로 여겨졌지만, 칩을 여러 층 쌓는 기술이 중요해지면서 위상이 달라졌다.

같은 뉴스, 다른 영향

  • "AI 투자 확대": 가속기 설계 회사와 그 칩을 만드는 파운드리, 가속기 옆에 붙는 고성능 메모리, 그 메모리를 쌓는 후공정 장비까지 이어진다. 반면 PC·스마트폰용 범용 칩 회사에는 직접 영향이 작다.
  • "설비 투자 축소": 칩 회사에는 공급 조절이라는 좋은 신호일 수 있지만 장비 회사에는 주문 감소다.
  • "수출 규제 강화": 특정 지역 매출 비중이 큰 회사와 그렇지 않은 회사가 갈린다. 규제 대상이 칩인지 장비인지에 따라서도 다르다.

지도를 쓰는 법

  • 관심 있는 회사가 어느 구역에 있는지, 매출의 얼마가 어느 구역에서 나오는지 사업보고서에서 확인한다. 한 회사가 여러 구역에 걸쳐 있는 경우도 많다.
  • 그 회사의 주요 고객과 공급사가 누구인지 적어 본다. 고객의 실적 발표가 내 관심 회사의 선행 지표가 된다.
  • 뉴스를 볼 때 "이 소식은 지도의 어느 구역 이야기인가"를 먼저 묻는다.

정리

반도체 산업은 하나의 긴 분업 사슬이다. 사슬의 어느 고리에 있는 회사인지 알면, 뉴스 제목의 "반도체 호재"나 "반도체 악재"가 내 관심 회사와 관련이 있는지부터 가려낼 수 있다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

메모리 반도체 실적은 왜 롤러코스터인가 — 사이클을 이해하는 최소한의 개념

메모리 반도체 회사는 어느 해에는 사상 최대 이익을 내고 몇 분기 뒤에는 적자를 낸다. 경영을 잘못해서가 아니라 산업의 구조가 그렇다. 이 구조를 모르면 가장 좋아 보일 때 들어가고 가장 나빠 보일 때 나오게 된다. 사이클이 생기는 이유를 차례로 정리한다.

1. 제품이 규격품이다

같은 규격의 D램은 어느 회사 것이든 서로 바꿔 쓸 수 있다. 차별화가 어려우니 가격은 수요와 공급으로 정해진다. 공급이 조금만 넘쳐도 가격이 크게 빠지고, 조금만 모자라도 크게 오른다.

2. 공급을 빨리 못 바꾼다

공장을 짓고 장비를 들여 생산을 시작하기까지 1~2년이 걸린다. 수요가 좋을 때 결정한 증설은 수요가 식은 뒤에 물량으로 나온다. 모든 회사가 같은 시기에 같은 판단을 하므로 과잉 공급이 한꺼번에 온다.

3. 고정비가 크다

공장과 장비의 감가상각비는 생산량과 상관없이 나간다. 가격이 원가 아래로 내려가도 공장을 세우는 것보다 돌리는 편이 손실이 작아서 생산은 계속된다. 그래서 가격 하락이 길어진다.

4. 고객이 재고로 증폭시킨다

가격이 오를 것 같으면 고객들은 미리 사서 쌓아 둔다. 실제 수요보다 주문이 많아진다. 가격이 내릴 것 같으면 쌓아 둔 재고를 먼저 쓰느라 주문을 끊는다. 실제 수요보다 주문이 적어진다. 재고가 사이클의 진폭을 키운다.

확인할 지표

  • 가격. 대형 고객과 분기 단위로 정하는 계약 가격과, 그때그때 거래되는 현물 가격이 있다. 현물이 먼저 움직이고 계약 가격이 뒤따르는 경우가 많다.
  • 재고. 실적 발표에서 회사와 고객의 재고 수준을 언급한다. "재고가 정상 수준으로 내려왔다"는 말은 회복의 초기 신호로 읽히곤 한다.
  • 설비 투자 계획. 회사들이 투자를 줄인다고 발표하면 몇 분기 뒤의 공급이 줄어든다는 뜻이다. 장비 회사의 수주 전망이 이를 간접적으로 보여 준다.
  • 제품 구성. 범용 제품과 고부가 제품의 비중. 같은 메모리라도 AI 서버용 고성능 제품은 가격 결정 방식이 다르다.

이번에는 다를까

AI 수요가 커지면서 "사이클이 사라졌다"는 주장이 나온다. 고성능 메모리는 고객과 미리 물량을 정하고 만들기 때문에 범용 제품보다 가격이 안정적이라는 근거다. 반대로 "수요의 종류가 바뀌었을 뿐 증설 경쟁은 똑같이 벌어지고 있다"는 반론도 있다. 어느 쪽이 맞는지는 지나 봐야 안다. 다만 사이클이 끝났다는 말은 과거 호황의 정점마다 나왔다는 사실은 기억해 둘 만하다.

주가는 실적보다 먼저 움직인다

메모리 회사의 주가는 대체로 실적이 가장 나쁠 때 바닥을 지나고, 실적이 가장 좋을 때 이미 꺾여 있었던 경우가 많다. 시장이 몇 분기 앞을 보기 때문이다. "실적이 이렇게 좋은데 왜 떨어지지", "적자인데 왜 오르지"라는 의문이 든다면 사이클의 어느 지점인지를 먼저 생각해 볼 일이다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

HBM 이 뭐길래 — AI 메모리가 반도체 회사 실적에 연결되는 경로

반도체 기사에 빠지지 않는 단어가 HBM 이다. 무엇인지는 대략 알아도, 이것이 왜 회사 실적과 주가 이야기의 중심에 있는지는 잘 설명되지 않는다. 기술보다 "돈이 흐르는 경로"를 중심으로 정리한다.

무엇인가

고대역폭 메모리(High Bandwidth Memory). D램 칩을 여러 층 수직으로 쌓고 층 사이를 미세한 구멍으로 연결해, 한 번에 주고받는 데이터의 양을 크게 늘린 메모리다. AI 가속기 바로 옆에 붙어서, 가속기가 계산할 데이터를 빠르게 공급한다. 가속기가 아무리 빨라도 메모리가 느리면 소용이 없기 때문에 둘은 짝으로 움직인다.

일반 메모리와 무엇이 다른가

  • 맞춤에 가깝다. 특정 가속기에 맞춰 개발하고 고객의 품질 인증을 받아야 납품할 수 있다. 인증을 통과한 공급사는 소수다.
  • 미리 팔고 만든다. 물량과 가격을 사전에 정하고 생산하는 비중이 크다. 그래서 범용 메모리보다 가격 변동이 작다고 알려져 있다.
  • 만들기 어렵다. 층을 쌓고 연결하는 후공정에서 불량이 나면 쌓은 칩 전체를 버린다. 수율이 곧 원가이고 공급 능력이다. "수율 몇 퍼센트 달성"이 기사 제목이 되는 이유다.
  • 웨이퍼를 많이 먹는다. 같은 용량의 일반 D램보다 훨씬 많은 생산 능력을 차지한다. HBM 을 늘리면 범용 D램 공급이 줄어 그쪽 가격까지 영향을 받는다.

실적으로 이어지는 경로

  1. 클라우드 회사들이 AI 설비 투자 계획을 발표한다.
  2. 가속기 주문이 늘고, 가속기 한 대에 들어가는 HBM 수요가 따라 늘어난다.
  3. 인증받은 메모리 회사의 고부가 제품 비중이 올라가 평균 판매 가격과 이익률이 높아진다.
  4. 쌓고 연결하는 후공정 장비와 소재 회사의 주문이 늘어난다.
  5. 범용 D램 공급이 빠듯해지면서 그쪽 가격도 영향을 받는다.

그래서 메모리 회사를 볼 때 맨 앞 단계, 곧 클라우드 회사들의 투자 계획 발표가 중요한 선행 신호가 된다.

기사에서 볼 숫자와 표현

  • 세대. 숫자와 알파벳으로 세대를 구분한다. 새 세대의 인증과 양산 시점이 회사 간 순위를 바꾼다.
  • 인증(퀄) 통과. 주요 고객의 품질 인증을 통과했는지, 어느 세대 제품인지.
  • 수율. 발표된 숫자가 어떤 기준인지 기사만으로는 알기 어렵다. 회사마다 기준이 달라 단순 비교는 조심해야 한다.
  • 패키징 증설. 후공정 생산 능력을 얼마나, 언제까지 늘리는지.

위험 요인

  • 고객 집중. 수요가 소수의 가속기 회사와 클라우드 회사에 몰려 있다. 그들의 계획이 바뀌면 영향이 크다.
  • 대체 설계. 추론용으로는 HBM 없이도 되는 구조를 내세우는 시도가 있다.
  • 증설 경쟁. 모든 공급사가 동시에 생산 능력을 늘리고 있다. 수요 증가가 둔해지는 순간 익숙한 과잉 공급이 재현될 수 있다.

정리

HBM 은 기술 용어이기 전에 "AI 투자 → 메모리 회사 이익"을 잇는 연결 고리다. 기사에서 이 단어를 만나면 세대, 인증, 수율, 증설 네 가지 중 무엇에 관한 소식인지를 가려 읽으면 된다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

반도체 3배 레버리지 ETF 는 왜 지수의 3배가 아닌가 — 일간 복리의 산수

반도체 지수의 하루 등락을 2배, 3배로 따라가는 레버리지 상품이 인기다. 그런데 한 달, 일 년을 놓고 보면 "지수는 제자리인데 내 상품은 손실"인 경우가 생긴다. 사기가 아니라 산수다. 계산기만 있으면 확인할 수 있는 내용을 정리한다.

약속은 "하루"의 배수다

레버리지 ETF 가 약속하는 것은 기초 지수의 하루 수익률의 배수다. 이틀 이상의 기간에 대해서는 아무 약속도 하지 않는다. 매일 장이 끝나면 배율을 다시 맞추기 때문에, 여러 날의 수익률은 매일의 배수 수익률을 곱한 값이 된다.

오르고 내리면 제자리가 아니다

지수가 첫날 10% 오르고 다음 날 10% 내렸다고 하자.

  • 지수: 100 → 110 → 99. 이틀 동안 −1%.
  • 3배 상품: 100 → 130 → 91. 이틀 동안 −9%.

지수 손실의 3배인 −3%가 아니라 −9%다. 순서를 바꿔 먼저 내리고 나중에 올라도 결과는 같다. 등락이 반복될수록 이 차이는 쌓인다.

횡보장이 가장 불리하다

지수가 20일 동안 하루는 +3%, 하루는 −3%를 번갈아 기록했다고 하자.

  • 지수: 하루 쌍마다 1.03 × 0.97 = 0.9991. 열 쌍이면 약 −0.9%.
  • 3배 상품: 하루 쌍마다 1.09 × 0.91 = 0.9919. 열 쌍이면 약 −7.8%.

지수는 거의 제자리인데 3배 상품은 8% 가까이 줄었다. 변동이 클수록, 기간이 길수록 이 깎임은 커진다. 반도체 지수는 변동이 큰 편이라 영향도 크다.

한 방향으로 계속 가면 반대로 유리하다

지수가 5일 연속 2%씩 오르면 지수는 약 +10.4%, 3배 상품은 1.06 을 다섯 번 곱해 약 +33.8%다. 3배인 31.2%보다 많다. 복리가 유리하게 작용한 것이다. 레버리지 상품의 장기 성과는 "지수가 얼마나 올랐나"만이 아니라 "얼마나 흔들리며 올랐나"에 달려 있다.

회복에 필요한 수익률

  • 지수가 하루에 20% 내리면 3배 상품은 60% 내린다. 100 이 40 이 된다.
  • 40 이 다시 100 이 되려면 150% 올라야 한다. 지수 기준으로는 그 3분의 1인 50%가 하루에 올라야 한다는 계산이 되지만, 현실에서는 여러 날에 걸쳐 오르므로 다시 복리 계산이 필요하다.
  • 이론상 지수가 하루에 33% 넘게 내리면 3배 상품의 가치는 0 에 가까워진다.

그 밖에 깎이는 것

  • 운용 보수. 일반 지수 상품보다 높다.
  • 배율을 만드는 비용. 파생 계약으로 배율을 만들 때 금리에 해당하는 비용이 든다. 금리가 높은 시기에는 이 비용이 커진다.
  • 환율. 해외 상품을 원화로 평가하면 환율 변동이 더해진다.
  • 국내 상장 상품의 시차. 해외 지수를 따라가는 국내 상품은 밤사이 지수 변동을 다음 날 아침에 한꺼번에 반영한다. 지수 변동에 배율을 곱하고 환율 변동을 더해 대략의 시초가를 가늠해 볼 수는 있지만 어디까지나 근사치다. 내가 만든 도구에서도 이 값에는 "근사"라고 표시해 둔다.

정리

레버리지 ETF 는 "지수의 3배"가 아니라 "매일 3배를 다시 거는 상품"이다. 방향을 맞혀도 흔들림이 크면 손실이 날 수 있다. 상품을 평가하기 전에 위의 계산을 직접 한 번 해 보면 구조가 몸에 들어온다. 이 글은 상품의 구조를 설명한 것이며 특정 상품의 매수나 매도를 권하지 않는다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

같은 종목인데 가격이 왜 다르지 — 정규장, 시간외, 대체거래소, 미국 주간거래

아침에 본 가격과 앱에 뜬 가격이 다르다. 미국 반도체주는 밤새 올랐다는데 내 화면의 등락률은 다른 숫자다. 시세를 모아 보여 주는 개인 도구를 만들면서, 이 혼란의 대부분이 "어느 시간대의 가격을, 무엇과 비교한 것인가"에서 온다는 것을 알게 됐다. 2026년 가을 기준으로 정리한다. 시간은 제도 변경으로 바뀔 수 있다.

한국 주식

  • 정규장. 한국거래소 기준 오전 9시부터 오후 3시 30분.
  • 대체거래소. 정규장 앞뒤로 거래할 수 있는 시장이 따로 있다. 아침 출근 시간대와 저녁 시간대의 체결가는 여기서 나온다.
  • 등락률의 기준. 대체거래소의 등락률은 전날 정규장 종가와 비교한 값이다.

내 도구에서 생긴 문제는 이랬다. 아침 8시대에 화면을 보면 미국 주식은 밤사이 결과가 반영돼 있는데 한국 종목은 어제 종가 그대로라 앞뒤가 맞지 않았다. 대체거래소의 장전 가격을 가져와 정규장 밖 시간에만 표시하도록 고쳐 해결했다. 이때 지켜야 했던 조건이 둘 있다. 오늘 체결된 값일 것, 그리고 정규장이 열려 있는 동안에는 쓰지 않을 것. 또 상장지수펀드는 이 가격 정보가 없는 경우가 많았고, 지수에는 장전 값이 없어서 "지수는 전날 마감 기준"이라는 주석을 따로 달았다.

미국 주식

  • 프리마켓. 미국 동부 시간 오전 4시부터 정규장 시작 전까지.
  • 정규장. 오전 9시 30분부터 오후 4시.
  • 애프터마켓. 오후 4시부터 8시.
  • 주간거래. 한국의 낮 시간에 일부 증권사가 제공하는 거래. 미국 현지는 밤이다.

실적 발표는 대부분 정규장이 끝난 뒤에 나온다. 그래서 큰 가격 변동은 애프터마켓에서 먼저 일어나고, 다음 날 정규장의 시작 가격에 반영된다.

도구를 만들며 밟은 함정

  • "이전 종가"의 정체. 무료 시세 자료의 요약 정보에 이전 종가처럼 보이는 값이 있었는데, 요청한 차트 범위가 시작되기 전의 종가였다. 이것으로 전일 대비를 계산하면 틀린다. 일별 자료의 마지막 두 값을 직접 비교해야 했다.
  • 주간거래 가격은 구하기 어렵다. 한국 낮 시간의 미국 주식 가격은 무료 자료 대부분에 없다. 그 시간에 내 화면은 애프터마켓 종가를 보여 주고 있었고, 증권사 앱의 가격과 몇 퍼센트씩 차이가 났다. 오래된 체결가에는 "주간거래 미반영"이라는 표시를 붙였다.
  • 주간거래가 일별 자료를 바꾼다. 어떤 자료원은 주간거래 체결이 그날의 일별 값에 섞여 들어와 "전일 종가"가 계속 변했다. 등락률의 기준은 정규장 종가로 고정하고, 주간거래 가격은 현재가 표시에만 쓰도록 나눴다.
  • 장 상태를 확인한다. 휴장일이나 장 시작 전에 "현재가"를 기준으로 계산하면 엉뚱한 값이 나온다. 장이 열려 있는지를 먼저 확인하고, 아니면 마지막 종가 기준이라고 표시한다.

가격을 볼 때 확인할 세 가지

  1. 언제 체결된 가격인가. 시각이 표시되지 않은 가격은 의심한다.
  2. 어느 시장의 가격인가. 정규장인지, 시간외인지, 대체거래소인지.
  3. 등락률은 무엇과 비교한 것인가. 전날 정규장 종가인지, 직전 시간외 종가인지.

정리

하루가 24시간 이어지는 시장이 되면서 "지금 가격"이라는 말이 여러 뜻을 갖게 됐다. 시간외 가격은 거래량이 적어 조금의 주문으로도 크게 움직인다는 점도 함께 기억해 둘 만하다. 숫자를 믿기 전에 그 숫자의 시각과 기준부터 확인하는 습관이 가장 싼 안전장치다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

수급 데이터 읽을 때 조심할 것 — 외국인·기관·프로그램·신용·대차·공매도

"외국인이 며칠째 순매수", "공매도 잔고 급증" 같은 수급 지표는 종목 분석에서 빠지지 않는다. 증권사 API 로 이런 데이터를 모으는 개인 도구를 만들어 보니, 지표의 뜻을 아는 것만큼 데이터가 어떻게 생겼는지를 아는 것이 중요했다. 해석 이전에 숫자 자체가 틀려 있는 경우가 많았기 때문이다.

지표가 말하는 것

  • 투자자별 매매. 외국인, 기관(그 안에서 연기금·투신·사모 등), 개인이 각각 얼마를 사고팔았는지. 누가 사는지를 보여 주지만 왜 사는지는 알려 주지 않는다.
  • 프로그램 매매. 미리 짜 둔 조건에 따라 여러 종목을 한꺼번에 주문하는 거래. 지수 관련 자금의 움직임을 짐작하는 데 쓴다.
  • 신용 잔고. 돈을 빌려 산 주식의 규모. 높으면 하락 때 강제 매도 물량이 나올 수 있다는 뜻으로 읽는다.
  • 대차 잔고. 빌려 간 주식의 규모. 공매도의 재료가 되지만 빌렸다고 모두 공매도하는 것은 아니다.
  • 공매도. 실제로 빌려서 판 수량과 비중.
  • 체결 강도. 매수 쪽 체결과 매도 쪽 체결의 비율.

데이터에서 겪은 함정

  • 장중에는 오늘 칸이 0 이다. 투자자별 매매 자료에서 당일 행이 장중에는 모두 0 으로 왔다. 이것을 그대로 쓰면 "연속 순매수 일수"가 낮 동안 0 으로 끊긴다. 전부 0 인 행은 빼고 읽어야 했다. 대차 잔고도 당일 값은 0 이었고, 신용 잔고는 다음 날에야 채워졌다.
  • 상장지수펀드는 비어 있다. 프로그램 매매, 신용, 대차, 공매도, 재무 지표가 상장지수펀드에는 빈 값이나 0 으로 왔다. 0 을 "수급이 없다"로 읽으면 안 되고 "해당 없음"으로 처리해야 한다.
  • 소수점이 없다. 어떤 조회에서는 등락률이 소수점 없이 왔다. 698 이 6.98%라는 뜻이었다. 같은 증권사의 다른 조회에서는 소수점이 있었다.
  • 부호가 방향 표시다. 현재가에 붙은 마이너스는 음수가 아니라 "전일보다 내렸다"는 표시였다. 절댓값을 써야 한다. 증감 항목에는 부호가 두 개 겹쳐 오는 값도 있어서 숫자로 바꿀 수 없으면 비워 두게 했다.
  • 종목 코드에 글자가 붙는다. 잔고 조회에서는 종목 코드 앞에 종류를 나타내는 글자가 붙어 있었다. 떼지 않으면 다른 자료와 연결되지 않는다.
  • 같은 종목이 여러 줄이다. 매수 방식에 따라 같은 종목이 여러 행으로 나뉘어 왔다. 합쳐야 한다.
  • 조회 한도. 계좌 관련 조회는 초당 한 건처럼 한도가 빡빡했다. 한도를 넘으면 오류가 나고, 그 오류를 "데이터 없음"으로 처리하면 화면에는 조용히 빈칸이 생긴다.

교차 확인

처음 붙인 뒤에는 포털의 수급 화면과 내 데이터의 부호를 한 달 치 대조해 봤다. 전부 일치하는 것을 확인하고서야 그 숫자로 다른 계산을 하기 시작했다. 한 번의 대조가 단위나 부호를 잘못 읽은 채 몇 달을 쓰는 일을 막아 준다.

해석에서 조심할 것

  • 수급은 결과이지 예측이 아니다. 연속 순매수는 지금까지의 일이다. 내일도 산다는 보장이 아니다.
  • 합계는 0 이다. 누군가 샀다면 누군가 팔았다. "외국인이 샀다"는 "개인이나 기관이 팔았다"와 같은 말이다.
  • 과거 검증의 한계. 호가 잔량이나 실시간 체결 강도처럼 과거 값이 남지 않는 지표는 과거 데이터로 검증할 수 없다. 내 도구는 그런 조건이 든 규칙을 과거에 대입하면 "이 조건은 항상 미충족으로 처리됨"이라는 경고를 띄운다. 경고가 없으면 검증한 것처럼 보이기 때문이다.

정리

수급 지표로 판단하기 전에 그 숫자가 언제 확정되는 값인지, 0 이 "없음"인지 "해당 없음"인지, 단위와 부호가 무엇인지를 먼저 확인해야 한다. 틀린 숫자 위의 분석은 아무리 정교해도 틀린 결론을 낸다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

백테스트 수익률을 그대로 믿으면 안 되는 이유 — 직접 만들어 보고 알게 된 일곱 가지

"이 규칙으로 과거 3년을 돌리면 수익률이 몇 퍼센트"라는 결과는 매력적이다. 과거 데이터에 매매 규칙을 대입해 보는 도구와 모의 매매 봇을 직접 만들어 운영하면서, 그 숫자가 실제와 벌어지는 지점을 여럿 확인했다. 규칙이 좋고 나쁨을 따지기 전에 검증 방법 자체에서 걸러야 할 것들이다.

1. 닿았다고 체결되는 것이 아니다

과거 데이터에서 그날 저가가 내 매수 지정가에 닿았으면 "체결"로 처리하기 쉽다. 실제로는 같은 가격에 먼저 걸려 있던 주문이 다 체결된 뒤에야 내 차례가 온다. 가격이 스치기만 한 날에는 체결되지 않았을 가능성이 크다. 나는 체결 기록마다 "가격이 지정가를 한 호가 이상 지나갔는지, 닿기만 했는지"를 따로 표시해 두고, 닿기만 한 체결을 뺀 결과도 함께 본다.

2. 비용은 생각보다 크다

매매 수수료와 세금은 거래할 때마다 나간다. 자주 사고파는 규칙일수록 비용이 수익을 갉아먹는다. 모의 투자 서버의 비용률이 실제와 다른 경우도 있었다. 내가 쓴 모의 서버는 실제보다 수수료가 훨씬 높게 잡혀 있었다. 그래서 같은 체결 기록에 모의 비용과 실제 비용을 각각 적용한 결과를 나란히 보여 주게 했다.

3. "아무것도 안 했다면"과 비교한다

수익이 났다고 규칙이 좋은 것은 아니다. 같은 기간에 처음 산 것을 그대로 들고 있었을 때의 결과와 비교해야 한다. 내 봇은 "첫날 수량을 끝까지 보유했다면"의 자산을 매일 함께 기록하고, 전체 손익을 "그냥 들고 있어서 생긴 몫"과 "사고팔아서 생긴 몫"으로 나눠 보여 준다. 나눠 보면 열심히 매매한 몫이 음수인 기간이 드물지 않다.

4. 그때 알 수 없던 정보를 쓰지 않는다

과거의 어느 날을 계산할 때는 그날까지의 자료만 써야 한다. 당연해 보이지만 실수하기 쉽다. 장 마감 뒤에 확정되는 값을 장중 판단에 쓴다든지, 나중에 수정된 수치를 당시 값처럼 쓰는 경우다. 조건을 평가하는 함수가 "그 날짜 이전의 값만 본다"를 구조적으로 보장하게 만들었다.

5. 과거 값이 없는 조건은 검증되지 않는다

호가 잔량이나 실시간 지표처럼 과거 기록이 남지 않는 조건이 규칙에 들어 있으면, 과거 검증에서는 그 조건이 항상 "미충족"으로 처리된다. 결과 표는 멀쩡히 나오지만 그 조건은 검증된 것이 아니다. 이런 조건이 있으면 결과 화면에 경고를 띄운다.

6. 값을 맞추다 보면 과거에만 맞는다

여러 값을 바꿔 가며 가장 좋은 결과를 찾으면, 그 값은 그 기간의 우연까지 학습한 것이다. 조합별 결과를 표로 볼 때는 최고점 하나가 아니라 "주변 값에서도 비슷하게 괜찮은가"를 본다. 한 칸만 유독 좋고 옆 칸이 나쁘면 우연일 가능성이 크다.

7. 하루치 재현은 하루치일 뿐이다

규칙을 바꾸면서 그날의 분 단위 자료로 옛 규칙과 새 규칙을 나란히 재현해 봤다. 옛 규칙의 재현 결과가 실제 기록과 거의 같아서 방법 자체는 쓸 만했다. 그래도 그것은 하루의 결과다. 그래서 새 규칙을 실제로 적용하면서, 옛 규칙을 같은 시세로 가상으로 계속 돌리는 대조군을 따로 두었다. 같은 날 같은 시점에 출발시켜야 비교가 공정하다.

정리

  • 체결 가정을 보수적으로: 닿기만 한 체결은 의심한다.
  • 비용을 실제 요율로 넣는다.
  • 아무것도 안 한 경우와 반드시 비교한다.
  • 검증되지 않은 조건이 무엇인지 표시한다.
  • 바꾼 규칙은 대조군과 함께 실시간으로 검증한다.

백테스트는 나쁜 규칙을 걸러내는 데는 쓸모가 있지만 좋은 규칙을 보장하지는 않는다. 이 글은 검증 방법에 관한 경험이며 특정 매매 방식을 권하지 않는다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

모의투자 API 로 자동매매 봇을 돌려 보고 알게 된 것 — 모의 서버와 실제의 차이

증권사가 제공하는 모의투자 API 로 반도체 종목 묶음을 규칙대로 사고파는 연습용 봇을 만들어 돌리고 있다. 실제 돈이 걸리지 않으니 마음 편히 실험할 수 있다. 다만 "모의에서 됐으니 실제도 된다"는 생각은 여러 번 깨졌다. 모의 환경의 특성과, 실제든 모의든 자동매매 프로그램이 갖춰야 할 것들을 적는다.

모의 서버는 실제와 다르다

  • 비용이 다르다. 내가 쓴 모의 서버는 수수료와 세금이 실제보다 높게 잡혀 있었다. 모의 수익률을 그대로 실제 기대치로 옮기면 안 된다.
  • 안 되는 주문이 있다. 일부 주문 유형, 시간외 거래, 대체거래소 주문이 "모의투자에서는 지원하지 않는다"는 오류로 거부됐다. 정규장의 기본 지정가 주문만 된다고 보는 편이 맞았다.
  • 장 시작 전 주문도 거부된다. 미국 주식은 정규장 전 시간대의 주문이 거부됐다. 처음에 이 오류를 "일시적이라 다시 시도할 오류" 목록에 넣지 않아서, 그날의 진입 시도가 "거부"로 기록되고 다시 시도되지 않았다.
  • 큰 주문은 나뉘어 체결된다. 수량이 큰 주문이 한 번에 체결되지 않고 일부만 체결된 채 남았다. "주문 = 즉시 전량 체결"을 가정한 코드는 여기서 꼬인다.
  • 잔고가 늦게 반영된다. 미국 주식을 산 직후에도 예수금 조회값은 그대로였다. 결제 전 금액이었기 때문이다. 잔고 조회에 기대지 않고, 시작 금액에 체결 내역을 더하고 빼는 자체 장부로 자산을 계산하게 바꿨다.

자동매매 프로그램이 갖춰야 했던 것

  • 목록은 끝까지 읽는다. 체결 내역 조회가 20건씩 끊겨 왔는데 첫 장만 읽고 있었다. 거래가 많았던 날 21번째부터의 체결을 통째로 놓쳤다. "다음 장 있음" 표시를 확인해 끝까지 읽어야 한다.
  • 주문은 함부로 다시 보내지 않는다. 주문 요청이 시간 초과로 실패해도 서버에는 접수됐을 수 있다. 그대로 다시 보내면 주문이 두 번 나간다. 잠시 뒤 당일 주문 목록에서 같은 주문(종목, 방향, 수량, 가격)을 찾아보고, 없을 때만 다시 낸다. 목록 조회마저 실패하면 중복 위험이 있으니 다시 내지 않는다.
  • 재시도할 오류와 아닌 오류를 나눈다. 통신 실패나 요청 한도 초과는 간격을 두고 다시 시도한다. "장이 끝났다", "잔고가 부족하다" 같은 업무상 거부는 다시 보내도 같으니 시도하지 않는다.
  • 장부를 먼저 쓰고 주문한다. 주문을 보낸 뒤 기록하면, 그 사이 프로그램이 죽었을 때 "보냈는지 모르는 주문"이 생긴다. 기록을 먼저 남긴다.
  • 같은 계좌로 두 개를 돌리지 않는다. 개발용과 운영용이 같은 계좌를 보면 주문이 두 번 나간다. 개발할 때는 주문을 보내지 않고 시세만 받아 가상으로 체결하는 모드를 따로 만들어 쓴다.
  • 기본값은 "꺼짐". 프로그램이 재시작돼도 자동매매가 저절로 켜지지 않게 한다. 사람이 버튼을 눌러야 주문이 나간다.
  • 실제 서버로 나갈 길을 없앤다. 접속 주소를 설정이 아니라 코드에 모의 서버로 고정하고 요청마다 확인한다. 설정 실수 한 번으로 실제 계좌에 주문이 나가는 일을 구조적으로 막는다.

모의로는 알 수 없는 것

  • 내 주문이 가격에 주는 영향. 모의 체결은 실제 호가를 움직이지 않는다.
  • 체결 순서. 같은 가격의 대기열에서 내 차례가 언제 오는지.
  • 심리. 실제 돈이 줄어드는 것을 보면서도 규칙을 지킬 수 있는지는 모의로 검증되지 않는다.

정리

모의투자는 규칙의 수익성보다 프로그램의 견고함을 검증하는 데 더 쓸모가 있었다. 부분 체결, 끊겨 오는 목록, 시간 초과, 거부 코드. 이런 것들을 실제 돈 없이 겪어 볼 수 있다는 것이 가장 큰 가치다. 이 글은 프로그램을 만든 경험을 적은 것이며 자동매매나 특정 종목 투자를 권하지 않는다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

반도체 실적 발표 시즌 읽는 법 — 좋은 실적에 주가가 떨어지는 이유

석 달마다 돌아오는 실적 발표 시즌에는 반도체 회사들의 주가가 하루에 크게 움직인다. "사상 최대 실적인데 급락", "적자인데 급등" 같은 제목이 매번 나온다. 실적 일정을 모아 보여 주는 화면을 직접 만들면서 정리해 둔, 발표를 읽는 순서를 적는다.

주가는 "기대 대비"로 움직인다

발표된 숫자가 좋으냐 나쁘냐가 아니라, 시장이 미리 예상한 것보다 좋으냐 나쁘냐가 주가를 움직인다. 예상치는 증권사 분석가들의 전망을 평균한 값(컨센서스)으로 대표된다. 사상 최대 실적이라도 예상보다 조금 못하면 실망으로 받아들여진다. 발표 전에 주가가 이미 많이 올랐다면 기대의 눈높이는 컨센서스보다도 높아져 있다.

보는 순서

  1. 지난 분기 실적. 매출과 이익이 예상치를 넘었는지. 가장 먼저 보도되지만 가장 덜 중요한 경우가 많다. 이미 지난 일이기 때문이다.
  2. 다음 분기 전망(가이던스). 회사가 스스로 제시하는 다음 분기 매출과 이익률 범위. 반도체 회사의 주가는 대체로 여기서 갈린다. 지난 분기가 좋아도 전망이 예상에 못 미치면 내리고, 지난 분기가 나빠도 "바닥을 지났다"는 전망이 나오면 오른다.
  3. 이익률. 매출이 늘었는데 이익률이 떨어졌다면 가격을 깎아 팔았거나 비용이 늘었다는 뜻이다.
  4. 재고와 설비 투자. 재고가 늘고 있는지 줄고 있는지, 내년 투자를 늘리는지 줄이는지. 사이클의 방향을 보여 준다.
  5. 질의응답. 발표 뒤 분석가들과의 문답에서 수요, 가격, 고객 재고에 대한 경영진의 표현이 나온다. 숫자보다 어조가 다음 날 기사 제목이 되곤 한다.

발표 순서가 만드는 연쇄

반도체 산업은 분업 구조라 한 회사의 발표가 다른 회사의 예고편이 된다.

  • 대형 클라우드 회사의 설비 투자 계획 → AI 가속기와 메모리 수요의 선행 신호.
  • 첨단 공정 장비 회사의 수주 → 파운드리와 메모리 회사의 증설 계획.
  • 먼저 발표하는 메모리 회사의 가격 전망 → 아직 발표하지 않은 경쟁사에 대한 기대.
  • 위탁 생산 회사의 월별 매출 → 그 고객인 설계 회사들의 출하 상황.

그래서 내가 가진 종목의 발표일만 볼 것이 아니라, 사슬에서 앞에 있는 회사들의 발표일도 캘린더에 함께 표시해 두면 도움이 된다.

캘린더를 만들며 알게 된 것

  • 발표 시각을 확인한다. 장 시작 전인지 장 마감 후인지에 따라 가격이 반응하는 시간대가 다르다. 미국 회사는 대부분 장 마감 후에 발표하므로 한국 시간으로는 새벽에서 아침 사이다.
  • 지난 날짜의 일정에는 결과가 채워진다. 무료로 공개된 실적 일정 자료 중에는 날짜가 지나면 실제 수치가 함께 표시되는 것이 있다. 예상치와 나란히 놓으면 한눈에 비교할 수 있다.
  • 경제 지표 일정도 같이 본다. 물가, 고용, 금리 결정 발표가 실적 발표와 같은 날 겹치면 어느 쪽 때문에 움직였는지 구분하기 어렵다.
  • 무료 자료는 호출 한도가 빡빡하다. 몇 분에 두세 번만 불러도 막히는 자료가 있어서 결과를 몇 시간씩 저장해 두고 쓴다.
  • 국내 회사의 발표 예정일은 구하기 어렵다. 미국과 달리 키 없이 받을 수 있는 정리된 일정 자료를 찾지 못했다. 공시와 회사 안내를 직접 확인해야 한다.

발표 당일의 변동성

실적 발표 직후의 가격은 시간외 거래에서 먼저 움직인다. 거래량이 적은 시간대라 진폭이 크고, 다음 날 정규장에서 방향이 바뀌는 일도 흔하다. 첫 반응이 최종 평가가 아니라는 것은 기억해 둘 만하다.

정리

실적 발표는 "얼마 벌었나"보다 "예상과 얼마나 달랐나", 그리고 "다음 분기를 어떻게 보나"로 읽는다. 사슬의 앞에 있는 회사들의 발표를 함께 보면 놀랄 일이 줄어든다. 이 글은 읽는 방법을 정리한 것이며 실적 발표를 이용한 매매를 권하지 않는다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

같은 날, 같은 사건, 정반대 제목 — 뉴스 헤드라인과 거리를 두는 방법

반도체 관련 뉴스를 자동으로 모아 보는 도구를 쓰다 보면 이상한 장면을 자주 본다. 같은 날 같은 사건을 두고 정반대 제목의 기사가 나란히 올라오는 것이다. 어느 날의 수집 목록에서 실제로 본 제목들을 예로, 헤드라인을 어떻게 다뤄야 하는지 정리한다.

하루에 나온 제목들

대형 반도체 기업의 경영진 교체 소식이 전해진 날, 국내 매체의 제목은 이렇게 갈렸다.

  • "동맹이 위태롭다", "미래 먹거리 어쩌나" 계열.
  • "떠나도 이상 없다", "오히려 새로운 기회" 계열.
  • "장중 급등했다가 하락 마감" 같은 시황 제목과, "떨어져도 국내 메모리주는 오른다"는 제목.

모두 같은 날 몇 시간 간격으로 나왔다. 어느 쪽을 먼저 봤느냐에 따라 전혀 다른 인상을 받게 된다.

왜 이렇게 되나

  • 제목은 시점에 묶여 있다. 오전에 오르면 오르는 이유를, 오후에 내리면 내리는 이유를 쓴다. 장중 시황 기사의 "이유"는 가격이 움직인 뒤에 붙는 설명이다.
  • 불확실할수록 양쪽 기사가 모두 나온다. 결과를 아무도 모르는 사건일수록 "위기"와 "기회"가 동시에 팔린다.
  • 제목은 클릭을 위해 쓰인다. 본문은 신중한데 제목만 단정적인 경우가 많다.
  • 같은 통신사 기사가 여러 매체에 실린다. 비슷한 제목이 열 개 보인다고 열 곳이 독립적으로 확인한 것이 아니다.

내가 가진 편향

더 큰 문제는 읽는 쪽에 있다. 가진 종목에 유리한 제목은 믿고 불리한 제목은 흘려보낸다. 불안할 때는 불안을 확인해 주는 제목만 눈에 들어온다. 뉴스가 판단의 근거가 아니라 이미 내린 판단의 장식이 된다.

도구를 만들며 정한 원칙

  • "왜 움직였나"는 참고로만. 크게 움직인 종목에 대해 관련 기사 제목을 모아 원인을 한 줄로 요약해 주는 기능을 만들어 쓰고 있다. 편리하지만 이것은 "기사들이 뭐라고 설명하는지"의 요약이지 실제 원인이 아니다. 화면에도 그렇게 구분해 둔다.
  • 요약에는 권고를 넣지 않는다. 요약을 쓰는 모델에게 사실만 정리하고 매수·매도 의견이나 전망은 쓰지 말라고 지시하고, 결과물에서도 한 번 더 거른다.
  • 판단 기준은 미리 정해 둔다. 어떤 조건에서 무엇을 할지는 뉴스를 보기 전에, 차분할 때 정한다. 뉴스가 나온 뒤에 기준을 바꾸고 싶어지면 그것은 대개 감정이다.
  • 기준을 바꿀 때는 기록한다. 무엇을, 왜 바꿨는지를 남기고, 바꾸기 전 기준을 가상으로 계속 돌려 비교한다. 바꾼 것이 나았는지를 나중에 확인할 수 있다.
  • 확인되지 않은 것은 표시한다. 늦게 반영되는 가격, 근사치로 계산한 값에는 화면에 꼬리표를 붙인다.

헤드라인을 읽는 요령

  1. 일차 자료를 찾는다. 공시, 실적 발표 자료, 회사의 공식 발표. 기사는 그것을 가리키는 안내판으로 쓴다.
  2. 사실과 해석을 나눈다. "누가 무엇을 발표했다"는 사실이고 "그래서 어떻게 될 것"은 해석이다.
  3. 반대 제목을 일부러 찾아본다. 반대 논리가 무엇인지 알고 나면 한쪽 제목의 확신이 줄어든다.
  4. 하루 묵힌다. 큰 사건일수록 첫날의 보도는 다음 날 수정된다.

정리

뉴스를 안 볼 수는 없다. 다만 헤드라인은 정보라기보다 그 순간의 분위기에 가깝다. 분위기에 따라 움직이지 않으려면 판단 기준을 뉴스 바깥에, 미리 세워 둬야 한다. 이 글은 정보를 다루는 방법에 관한 개인적인 정리이며 투자 판단을 권하지 않는다.

이 글은 산업과 제도, 도구를 만든 경험을 설명하기 위한 것이며 특정 종목이나 상품의 매수·매도를 권하지 않는다. 제도와 수치는 글을 쓴 시점(2026년 11월) 기준으로 바뀔 수 있고, 투자 판단과 그 결과는 각자의 책임이다.

엔비디아 CEO 사임 보도 이후, 한국 정부와 업계는 무엇을 점검하고 있나 — 보도 정리

엔비디아의 젠슨 황 최고경영자가 사임했다는 소식이 주요 외신을 통해 전해졌다(The New York Times, BBC, Reuters 등). 재무 책임자가 임시로 경영을 맡고 후임 선임 절차가 시작됐다는 보도도 함께 나왔다(Reuters, Axios). 이 글은 그 소식 자체보다, 같은 날 한국에서 나온 반응 보도를 정리한다.

정부: "GPU 공급과 협력 유지" 점검

  • 정부가 긴급 점검에 나섰고 GPU 공급과 협력 관계 유지에 힘을 쏟는다는 보도가 있었다(매일경제).
  • 국가 AI 전략을 다루는 위원회도 GPU 공급과 국내 기업 협력에 미칠 영향을 살핀다고 한다(뉴시스).
  • 이번 일을 계기로 국산 AI 반도체 육성을 서두른다는 보도도 나왔다(OhmyNews).

업계: 협력 관계가 사람에 묶여 있었나

국내 메모리 업체와 엔비디아의 관계는 그동안 최고경영자 사이의 친분으로 자주 묘사돼 왔다. 그래서 국내 기사 제목에는 "동맹이 흔들리나"라는 물음이 많았다(EBN, 이뉴스투데이, 에너지경제신문). 반대로 "CEO 가 떠나도 회사는 남는다", "메모리 공급에는 이상이 없다"는 분석 기사도 같은 날 나왔다(S저널, 파이낸셜포스트).

시장: 하루 안에 엇갈린 움직임

국내 증시는 장중에 크게 올랐다가 하락으로 마감했고 대형 메모리 종목이 약세였다는 보도가 이어졌다(이투데이, 스페셜타임스). 한편 "엔비디아가 떨어져도 국내 메모리주는 오른다, 관건은 AI 투자가 이어지느냐"는 제목의 기사도 있었다(매일경제). 같은 날 같은 사건을 두고 정반대 제목이 나온다는 것 자체가, 아직 방향이 정해지지 않았다는 뜻으로 읽힌다.

지켜볼 점

  • 후임 인선과 기존 계약. 공급 계약과 차세대 제품 일정은 개인이 아니라 회사 사이의 약속이다. 후임 체제가 기존 로드맵을 유지한다고 밝히는지가 첫 신호가 될 것이다.
  • GPU 확보 계획. 공공과 민간이 세워 둔 GPU 도입 계획에 일정 변화가 생기는지.
  • "대안" 논의의 지속성. 국산 AI 반도체나 다른 공급사를 키우자는 논의는 위기 때마다 나왔다가 잦아들곤 했다. 이번에는 예산과 구매로 이어지는지 볼 일이다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

HBM 경쟁 구도는 어떻게 바뀌나 — 삼성·SK·인텔·중국 업체 관련 보도 정리

AI 가속기 옆에 붙는 고대역폭 메모리(HBM)는 지금 AI 반도체 공급망에서 가장 빡빡한 고리로 꼽힌다. 엔비디아 경영진 교체 보도와 맞물려 HBM 경쟁 구도를 다룬 기사가 하루에 여럿 나왔다. 흩어진 소식을 한자리에 모았다.

기존 두 강자

  • SK하이닉스가 차세대 제품인 HBM4 에서 수율 70%를 달성했다는 보도가 있었다(digitimes). 같은 묶음에서 국내 화학 업체가 HBM 적층 공정용 소재 개발에 착수했다는 소식도 전해졌다(digitimes).
  • 국내 매체는 이번 국면을 "삼성의 반격, SK의 수성"으로 요약했다(서울파이낸스). 차세대 규격(HBM4E) 개발에 경영진 교체가 변수가 될 수 있다는 기사도 있었다(이투데이).

HBM 을 우회하려는 움직임

  • 인텔이 "HBM 이 필요 없는 추론용 GPU"를 내세워 국내 기업 23곳과 협력체를 꾸렸다는 보도가 나왔다(digitimes).
  • SK하이닉스와 인텔이 AI 서버의 병목을 줄이기 위해 메모리를 공유하는 구조를 검토한다는 기사도 있었다(digitimes).

학습용 최상위 가속기는 HBM 없이는 어렵지만, 이미 학습된 모델을 돌리는 추론은 요구 조건이 다르다. 추론 수요가 커질수록 "꼭 가장 비싼 메모리여야 하는가"라는 질문이 설계 단계에서 나오는 것은 자연스럽다.

중국 업체와 규제

  • 중국의 메모리 업체 CXMT 가 AI 서버용 메모리와 차세대 모바일 D램 성과를 앞세워 고부가 시장을 겨냥한다는 보도가 있었다(digitimes).
  • 동시에 미국 상원의원이 이 회사에 대한 수출 통제와 미국 시장 진입 금지를 촉구했다는 소식도 전해졌다(Businesskorea, CNBC).

장비와 주변 생태계

  • 반도체 장비 업체 어플라이드 머티어리얼즈가 D램 장비 매출 호조로 최대 실적을 냈고 내년 장비 수요 전망을 높였다는 보도가 있었다(Chosunbiz).
  • 웨이퍼 크기의 칩으로 알려진 세레브라스가 대형 계약을 맺고 내년 여름 차세대 칩 양산을 추진한다는 소식도 있었다.

정리

하루치 제목만 놓고 봐도 흐름은 세 갈래다. 기존 강자의 세대 교체 경쟁, HBM 의존을 줄이려는 설계 쪽 시도, 그리고 규제가 가르는 시장 경계. 어느 한 회사의 인사 문제보다 이 세 흐름이 몇 년 단위의 판도를 정할 가능성이 크다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

삼성의 AI 반도체 투자 보도 모아 보기 — 충청권 벨트, 패키징 거점, 데이터센터 냉각

같은 날 삼성 관련 투자 소식이 여러 매체에서 따로따로 나왔다. 숫자와 지명이 기사마다 달라 헷갈리기 쉬워서, 보도된 내용을 항목별로 나눠 적는다. 매체마다 범위와 집계 기준이 다를 수 있다는 점을 먼저 밝혀 둔다.

충청권 "AI 반도체 벨트"

삼성이 충청권에 AI 반도체 벨트를 만들며 144조 원을 투입하고, 협력사와 소재·부품·장비 분야 투자를 넓힌다는 보도가 있었다(빅데이터뉴스).

HBM 패키징 거점

  • 천안 부지가 부족해 온양에 46조 원을 들여 HBM 패키징 거점을 만들고 가동 시점을 2028년으로 앞당긴다는 보도가 해외 매체에서 나왔다(digitimes, TrendForce, Korea IT Times).
  • 국내 매체에서는 아산 HBM 생산시설 착공 소식과 함께 공장 건설 6조 원, 장비 40조 원이라는 숫자가 보도됐다(얼리어답터뉴스).

온양은 아산시에 속한 지역이므로 두 보도는 같은 사업을 다른 이름과 다른 구분으로 전한 것일 가능성이 있다. 46조와 6조+40조가 맞아떨어진다는 점도 그렇다. 다만 제목만으로 단정할 수는 없다.

데이터센터 냉각

삼성전자가 미국의 냉각 설비 업체 인수를 협상 중이라는 보도가 블룸버그발로 여러 매체에 실렸다(지디넷코리아, 재경일보). 앞서 인수한 유럽 공조 업체에 이어 데이터센터 열관리 사업을 넓히는 행보라는 설명이 붙었다.

왜 패키징과 냉각인가

  • 패키징. HBM 은 메모리 칩을 여러 층 쌓아 올려 만든다. 칩을 만드는 공정만큼이나 쌓고 연결하는 후공정이 수율과 생산량을 좌우한다. 생산 능력의 병목이 후공정에 있다면 투자가 그쪽으로 가는 것이 당연하다.
  • 냉각. AI 서버는 한 랙에 들어가는 전력이 기존 서버의 몇 배다. 공기로 식히는 데 한계가 와서 액체 냉각이 빠르게 퍼지고 있다. 칩을 파는 회사가 식히는 설비까지 갖추면 데이터센터 사업자에게 묶음으로 제안할 수 있다.

지켜볼 점

  • 발표된 금액이 몇 년에 걸친 계획인지, 이미 집행 중인 투자가 얼마나 포함됐는지.
  • 협력사 투자 확대가 실제 발주로 이어지는 시점.
  • 냉각 업체 인수 협상의 성사 여부. "협상 중" 보도는 결과가 달라지는 일이 흔하다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

AI 칩 수출 통제 논란 정리 — 우회 유입 의혹, 의회 조사, 그리고 "모델에 남은 지문"

미국이 수출을 막은 고성능 AI 칩이 제3국을 거쳐 중국으로 들어갔다는 의혹이 다시 커지고 있다. 정치, 기업 회계, 기술 분석이 한꺼번에 얽힌 사안이라 하루치 보도만으로도 갈래가 많다. 보도된 사실 관계만 추려 본다.

수사와 조사

  • 싱가포르에서 엔비디아 칩 관련 사건으로 다섯 번째 기소가 이뤄졌다는 보도가 있었다(Bloomberg). 이 논쟁에 싱가포르가 계속 등장하는 이유를 짚은 해설 기사도 나왔다(CNBC).
  • 말레이시아 당국이 관련 업체의 자회사를 AI 반도체 유출 혐의로 조사한다는 소식이 전해졌다(연합뉴스).
  • 미국 의회가 대중국 AI 칩 유입 의혹과 관련해 상무장관과 수출 승인 과정을 조사한다는 보도가 여럿 나왔다(NBC News, Financial Times, CBS News). 장관 경질 검토 보도도 있었다(스페셜경제).
  • 엔비디아가 조사가 진행되는 가운데 싱가포르 매출의 회계 보고 방식을 바꿨다는 보도도 있었다(Fortune).

규제 방향

  • 미 의회의 규제 강화 움직임으로 중국의 칩 접근이 불투명해졌다는 분석(The Japan Times), 정상회담을 앞두고 규제가 더 세질 수 있다는 전망(KB Think)이 나왔다.
  • 주요 AI 모델 기업 두 곳이 대중국 AI 반도체 수출 규제를 지지하는 입장을 냈다는 보도도 있었다(뉴스피릿).

기술 쪽에서 나온 이야기

개발자 커뮤니티에서는 "중국의 최신 AI 모델에서 수출 통제 대상 GPU 의 흔적이 발견됐다"는 글이 큰 관심을 받았다(Hacker News, GeekNews). 모델의 출력이나 가중치에 남은 수치적 특성으로 어떤 하드웨어에서 계산했는지를 추정할 수 있다는 주장이다. 제목만으로 방법의 타당성을 판단할 수는 없지만, 통관 서류가 아니라 결과물에서 증거를 찾는 접근이라는 점이 눈에 띈다.

왜 막기 어려운가

칩은 작고 비싸며, 서버에 꽂힌 채로 팔린다. 합법적으로 수입한 나라에서 조립된 서버가 다시 수출되면 최종 목적지를 추적하기 어렵다. 그래서 통제는 판매 시점의 승인보다 사후 추적과 중간 경유국의 협조에 기대게 되고, 이번 보도들에 여러 나라의 수사 당국이 등장하는 것도 그 때문이다.

다루지 않은 것

같은 날 특정 개인의 신원과 사생활, 정보기관 연루를 주장하는 기사 제목이 많았다. 확인되지 않은 주장이 섞여 있고 사실로 드러나지 않은 개인 관련 의혹을 옮기는 것은 적절하지 않아 이 글에서는 뺐다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

Anthropic 상장 관련 보도 정리 — 개발자 입장에서 눈여겨볼 대목

AI 모델 기업 Anthropic 의 기업공개 소식이 개발자 커뮤니티와 경제 매체 양쪽에서 크게 다뤄졌다. Hacker News 에서는 그날 가장 많은 추천을 받은 글이었다. 숫자는 매체가 전한 대로만 옮기고, 이 소식이 모델을 쓰는 쪽에 어떤 의미인지를 중심으로 정리한다.

보도된 내용

  • 공모 목표가가 주당 70~78달러이고 연환산 매출이 810억 달러에 이르렀다는 요약 기사가 있었다(TradingView).
  • 높은 기업가치가 곧 기업 고객이 내는 AI 비용의 증가를 뜻한다는 분석이 나왔다(CX Today).
  • 엔비디아의 혼란으로 대형 클라우드 사업자는 이익을 볼 수 있지만 Anthropic 은 불리한 위치라는 해설도 있었다(Techzine Global).
  • 국내에서는 이 상장이 메모리 수요 확대 기대로 이어진다는 기사가 나왔다. 다만 공급 부족이 변수라는 단서가 붙었다(녹색경제신문).

쓰는 쪽에서 눈여겨볼 대목

  • 가격. 상장 기업은 분기마다 성장과 수익성을 설명해야 한다. 지금까지 모델 가격은 세대가 바뀔 때마다 같은 성능 기준으로 내려왔지만, 최상위 모델의 가격과 구독 상품의 한도 정책이 어떻게 움직일지는 지켜봐야 한다. 비용이 오를 수 있다는 분석이 나온 만큼, 모델 등급을 나눠 쓰고 호출량을 계측하는 습관이 더 중요해진다.
  • 연산 자원 의존. 모델 기업은 칩을 직접 만들지 않는다. 가속기 공급사와 클라우드 사업자에 기대는 구조여서, 공급 쪽 혼란이 서비스 용량과 속도 제한으로 번질 수 있다. 한 벤더에만 묶이지 않도록 호출 계층을 얇게 추상화해 두면 이런 때 선택지가 생긴다.
  • 공개되는 정보. 상장 과정에서는 매출 구성, 주요 고객 집중도, 비용 구조가 문서로 공개된다. 그동안 추정에 의존하던 "어디서 돈을 버는가"가 드러나면, 어떤 제품에 투자가 집중될지도 읽을 수 있다.

같은 날 나온 다른 목소리

이 회사 출신 연구원이 AI 개발 속도를 늦춰야 한다고 촉구했다는 보도(The Stanford Daily), AI 기업들이 자사 기술을 통제하지 못한다고 주장하는 소송이 제기됐다는 보도(Anadolu Ajansı)도 같은 날 있었다. 성장의 숫자와 위험에 대한 경고가 나란히 나오는 것이 지금 이 산업의 모습이다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

구글 Gemini 4 Hydrogen 공개 보도 — "속도 특화 모델"이 따로 나오는 이유

구글이 속도에 초점을 맞춘 새 모델 "Gemini 4 Hydrogen"을 공개했다는 보도가 나왔다(Mashable). 개발자 커뮤니티에서도 관련 글 두 건이 상위에 올랐고, 그중 하나는 "Orchestrator"라는 이름과 함께 소개됐다(Hacker News). 모델 자체의 성능은 직접 써 보기 전에는 말하기 어려우니, 여기서는 왜 요즘 "빠른 모델"이 별도 제품으로 나오는지를 정리한다.

빠른 모델이 필요한 자리

  • 에이전트의 반복 호출. 에이전트는 한 가지 일을 하려고 모델을 수십 번 부른다. 파일을 읽고, 검색하고, 결과를 확인하는 단계마다 호출이 일어난다. 한 번에 10초 걸리는 모델로는 전체 작업이 몇 분씩 늘어진다.
  • 음성과 실시간 화면. 말로 묻고 답하는 기능은 1초만 늦어도 어색하다. 가장 똑똑한 모델보다 충분히 똑똑하고 빠른 모델이 낫다.
  • 대량 처리. 분류, 번역, 추출처럼 단순하고 양이 많은 작업은 속도가 곧 비용이다.

"지휘자"라는 이름이 붙는 이유

함께 언급된 Orchestrator 라는 이름은 최근의 흐름을 잘 보여 준다. 큰 모델 하나가 모든 것을 하는 대신, 계획을 세우는 모델이 일을 쪼개어 빠른 모델 여럿에게 나눠 주는 구조다. 제목만으로 구체적인 기능은 알 수 없지만, 여러 회사가 비슷한 방향의 제품을 내고 있다.

직접 써 보며 배운 것

개인 프로젝트에서 모델을 큰 것과 가벼운 것 두 단으로 나눠 쓰고 있는데, 그 경험에서 몇 가지를 덧붙인다.

  • 가벼운 모델은 추론 과정을 끄면 훨씬 빨라지지만, 길이 지시를 잘 안 지키고 프롬프트 뒤쪽의 조건을 흘리는 경향이 있었다. 중요한 조건은 맨 앞에 두고, 출력은 코드로 한 번 검사한다.
  • 첫 응답은 빠른 모델로 먼저 내보내고 본문은 큰 모델이 이어 쓰게 하면, 체감 대기 시간이 크게 준다.
  • 자주 도는 작업을 가벼운 모델로 내리는 것이 비용에 가장 큰 영향을 줬다.

지켜볼 점

  • 공개된 속도 수치가 실제 사용 조건(긴 입력, 도구 호출 포함)에서도 유지되는지.
  • 가격. 빠른 모델이 싸기까지 하면 기본 선택이 바뀐다.
  • 다른 회사들의 대응. 한 곳이 속도 경쟁을 걸면 보통 몇 달 안에 비슷한 등급이 뒤따른다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

내 기기에서 도는 AI — 개인 컴퓨터용 모델, 통합 API, NPU 협력 보도 정리

클라우드의 초대형 모델 소식에 가려지기 쉽지만, 같은 날 개발자 커뮤니티의 상위권에는 "내 기기에서 돌리는 AI" 이야기가 여럿 있었다. 따로 보면 작은 소식이지만 묶으면 하나의 흐름이다.

보도·게시된 내용

  • 개인 컴퓨터용 대형 모델. "개인 컴퓨터에서 쓰는 최첨단급 AI"를 내세운 Edge0-145B 라는 모델 소개 글이 두 커뮤니티에서 모두 상위에 올랐다(Hacker News, GeekNews).
  • 작고 오래된 기기에서의 시도. 작은 기기와 구형 기기에서 언어 모델을 돌려 본 실험 글도 많은 추천을 받았다(Hacker News).
  • 기기 안과 클라우드를 한 API 로. 온디바이스 모델과 클라우드 모델을 같은 방식으로 호출하게 해 주는 Swift 용 프레임워크가 소개됐다(GeekNews).
  • NPU 협력. 국내 AI 반도체 기업이 인텔과 손잡고 인텔의 추론 도구와 자사 NPU 를 연동한다는 보도가 있었다(아이티비즈).

왜 다시 기기 안인가

  • 비용. 호출할 때마다 돈이 나가는 구조는 사용량이 늘수록 부담이다. 한 번 사 둔 하드웨어에서 도는 모델은 추가 비용이 전기료뿐이다.
  • 개인정보. 사진, 메모, 음성처럼 밖으로 내보내기 꺼려지는 데이터는 기기 안에서 처리하는 것이 가장 깔끔하다.
  • 지연과 오프라인. 네트워크를 거치지 않으면 반응이 빠르고, 연결이 끊겨도 동작한다.
  • 공급 불안. 클라우드 GPU 가 귀하고 정책이 자주 바뀌는 시기에는 내 손에 있는 연산 자원의 가치가 올라간다.

현실적인 한계

  • 파라미터가 수백억 개를 넘는 모델은 양자화를 해도 수십 기가바이트의 메모리가 필요하다. "개인 컴퓨터"라고 해도 사양이 높은 기기를 뜻하는 경우가 많다.
  • 속도는 여전히 클라우드보다 느리고, 긴 문맥에서는 차이가 더 벌어진다.
  • 도구 호출, 긴 작업의 일관성 같은 에이전트 능력은 모델 크기에 민감하다.

섞어 쓰는 구조가 자리 잡는 중

통합 API 가 나온다는 것은 "기기 안이냐 클라우드냐"를 고르는 시대에서 "작업에 따라 자동으로 나눠 보내는" 시대로 넘어간다는 신호다. 간단한 요약과 분류는 기기에서, 어려운 추론은 클라우드에서. 앱을 만드는 입장에서는 처음부터 모델 호출 부분을 갈아 끼울 수 있게 짜 두는 것이 안전하다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

에이전트가 결제하고, 기억하고, 길을 잃지 않게 — 에이전트 생태계 소식 정리

AI 에이전트 관련 소식은 이제 "에이전트가 무엇을 할 수 있나"에서 "에이전트가 일하려면 주변에 무엇이 필요한가"로 옮겨 가고 있다. 같은 날 올라온 소식 네 가지가 그 방향을 잘 보여 준다.

1. 에이전트가 직접 돈을 낸다

한 검색 API 가 x402 프로토콜로 에이전트의 결제를 받기 시작했다는 글이 올라왔다(Hacker News). x402 는 "결제 필요"를 뜻하는 HTTP 상태 코드 402 를 활용해, 요청에 대한 응답으로 가격을 알려 주고 결제가 확인되면 결과를 주는 방식이다. 사람이 미리 가입해 키를 발급받는 절차 없이 에이전트가 쓰는 만큼 내는 구조를 목표로 한다.

편리해 보이지만 따져 볼 것이 많다. 에이전트에게 얼마까지 쓰게 할지, 잘못된 반복 호출로 돈이 새는 것을 어떻게 막을지. 내 경험으로는 사람 손을 떠난 자동 호출에는 반드시 일일 상한이 있어야 했다.

2. 도구 연결 표준 위에 쌓이는 것들

MCP 로 연결하는 실시간 멀티모달 컨텍스트 도구가 소개됐다(GeekNews). MCP 는 에이전트와 외부 도구를 잇는 표준으로 자리를 잡았고, 이제 그 위에 화면, 음성, 실시간 상태 같은 풍부한 맥락을 실어 나르려는 시도가 이어지고 있다. 표준이 정해지면 그 위의 생태계가 빨리 자란다는 것을 보여 준다.

3. 긴 작업에서 목표를 잃지 않게

긴 작업 중에 에이전트가 원래 목표를 잊지 않도록 돕는 실행 틀(하네스)이 소개됐다(GeekNews). 에이전트를 오래 돌려 본 사람이라면 공감할 문제다. 작업이 길어지면 초반의 지시가 문맥에서 밀려나고, 에이전트는 눈앞의 오류를 고치는 데 몰두하다 엉뚱한 곳에 도착한다.

도구 없이도 쓸 수 있는 방법이 있다. 목표와 완료 조건을 파일에 적어 두고 단계마다 다시 읽게 하는 것, 그리고 진행 상황을 장부로 남기게 하는 것이다.

4. "직원마다 에이전트"

한 네트워크 장비 기업이 인원을 줄이고도 이익이 늘었고, 최고경영자가 직원마다 AI 에이전트를 배정한다고 말했다는 보도가 있었다(Benzinga). 기업 발표의 표현은 걸러 들어야 하지만, 에이전트가 실험 단계를 지나 인력 계획의 언어로 이야기되기 시작했다는 점은 분명하다.

공통점

네 소식 모두 모델 자체의 이야기가 아니다. 결제, 맥락 전달, 작업 관리, 조직 배치. 모델이 충분히 똑똑해진 뒤에 남는 문제는 결국 "일을 시키는 구조"라는 것을 보여 준다. 에이전트를 도입하려는 팀이라면 어떤 모델을 쓸지보다, 권한과 한도와 기록을 어떻게 설계할지를 먼저 정하는 편이 낫다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

AI 위험 경고와 대규모 장애 보도 — 의존이 깊어질수록 물어야 할 것

AI 산업의 성장 소식이 쏟아진 날, 반대편의 소식도 함께 나왔다. 개발 속도를 늦추자는 경고, 기업의 통제 능력을 문제 삼는 소송, 그리고 널리 쓰이는 업무 서비스의 대규모 장애다. 성격은 다르지만 "우리는 이것에 얼마나 기대고 있는가"라는 같은 질문으로 이어진다.

보도된 내용

  • 속도 조절 촉구. 주요 AI 기업 출신 연구원이 기술의 위험을 경고하며 개발 속도를 늦출 것을 촉구했다(The Stanford Daily).
  • 통제 능력을 묻는 소송. 주요 AI 기업들이 자사 기술을 통제하지 못한다고 주장하는 소송이 제기됐다(Anadolu Ajansı).
  • 대규모 장애. 마이크로소프트의 업무용 서비스에 전 세계적인 장애가 발생해 메일, 협업 도구와 함께 AI 서비스도 영향을 받았다는 보도가 있었다(Data Center Dynamics).
  • 일상으로 들어온 챗봇. 배우자와 그의 챗봇을 다룬 긴 기사도 눈에 띄었다(Bloomberg). 제목만으로 내용을 단정할 수 없지만, AI 와의 관계가 가정의 이야기로 다뤄질 만큼 가까워졌다는 것을 보여 준다.

먼 위험과 가까운 위험

앞의 두 소식은 장기적이고 구조적인 위험에 관한 것이고, 장애는 오늘 당장의 위험이다. 논쟁은 주로 앞쪽에서 벌어지지만 실무에서 먼저 마주치는 것은 뒤쪽이다. 메일과 메신저와 AI 보조 기능이 한 회사의 한 인프라에 올라가 있으면, 한 번의 장애로 전부가 함께 멈춘다.

작은 서비스를 운영하며 정해 둔 것

개인 프로젝트 몇 개에 AI 기능을 붙여 운영하면서 지키는 원칙이 있다. 큰 조직의 대책과는 규모가 다르지만 생각의 방향은 같다.

  • AI 가 멈춰도 기능은 남는다. 요약 생성이 실패하면 미리 만든 단순한 문장으로 대신한다. 음성 합성이 막히면 기기의 기본 음성으로 읽는다. 덜 똑똑해질 뿐 멈추지 않는다.
  • 재시도에는 끝이 있다. 서비스가 "한참 뒤에 다시 시도하라"고 답하면 기다리지 않고 예비 경로로 넘긴다. 무한히 기다리는 코드는 장애를 내 쪽으로 옮겨 온다.
  • 결정과 실행은 맡기지 않는다. 모델은 정리하고 설명한다. 기준을 정하고 실행하는 것은 사람이거나, 사람이 정한 규칙이다.
  • 나가는 데이터를 줄인다. 모델에게 꼭 필요하지 않은 민감한 값은 처음부터 보내지 않는다.

정리

경고와 소송이 옳은지 그른지는 시간이 지나야 알 수 있다. 하지만 장애는 반드시 다시 온다. AI 에 일을 더 맡길수록 "이것이 없을 때 우리는 어떻게 일하는가"를 미리 답해 두는 것이, 거창한 논쟁보다 먼저 할 수 있는 준비다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

AI 가 안경으로 들어온다 — 삼성의 AI 안경 공개 보도와 따져 볼 것들

삼성전자가 구글의 AI 를 얹은 스마트 안경을 공개했다는 보도가 여러 매체에서 나왔다. AI 가 화면 속 대화창을 벗어나 몸에 걸치는 기기로 옮겨 가는 흐름의 한 장면이다.

보도된 내용

  • "보는 것에 답하는" AI 안경 30종을 공개했다는 보도가 있었다(더글로벌기빙뉴스).
  • 국내 출시일을 12월 10일로 전한 기사가 있었다(비즈니스플러스).
  • 가격과 저장 공간에서 경쟁 제품을 앞선다는 기사도 나왔다(CWN). 비교 대상과 조건은 원문을 봐야 한다.
  • 같은 시기 국내에서는 반도체, 가전, 로봇을 한자리에 모은 AI 전시 행사 현장 기사도 있었다(뉴스핌).

왜 안경인가

멀티모달 모델은 글뿐 아니라 사진과 소리를 함께 이해한다. 그 능력을 가장 자연스럽게 쓰는 방법은 사용자가 보고 듣는 것을 그대로 모델에게 보여 주는 것이다. 휴대전화를 꺼내 사진을 찍고 질문을 입력하는 과정이 사라지고, "이게 뭐야?"라고 묻기만 하면 된다. 손이 자유롭지 않은 상황(요리, 수리, 운전 중 정차)에서 쓸모가 크다.

따져 볼 것들

  • 주변 사람의 사생활. 카메라가 달린 안경은 쓰는 사람보다 찍히는 사람의 문제다. 촬영 중임을 알리는 표시가 얼마나 분명한지, 얼굴 인식 같은 기능이 어디까지 허용되는지가 제품마다 다르다.
  • 처리 위치. 보고 들은 것이 기기 안에서 처리되는지, 서버로 올라가는지. 서버로 간다면 얼마나 보관되고 학습에 쓰이는지. 약관에서 가장 먼저 확인할 부분이다.
  • 배터리와 발열. 안경은 무게와 크기의 제약이 심하다. 상시 대기하는 AI 기능이 하루를 버티는지는 사양표가 아니라 실사용 후기로 봐야 한다.
  • 소리 입출력. 음성 기능을 직접 만들어 보니 어려운 것은 모델이 아니라 주변 환경이었다. 시끄러운 곳에서 말이 끝난 시점을 알아내는 것, 블루투스 기기와 마이크를 함께 쓸 때 생기는 문제 같은 것들이다. 안경도 같은 숙제를 안고 있을 것이다.
  • 연결되는 생태계. 특정 회사의 AI 가 실린 기기는 그 회사의 계정과 서비스에 묶인다. 이미 쓰는 서비스와 맞는지 볼 필요가 있다.

정리

AI 안경은 몇 년째 "곧 온다"던 제품군이다. 대형 제조사가 수십 종을 한꺼번에 내놓는다는 것은 시험 단계를 지났다는 신호로 볼 수 있다. 다만 새 범주의 기기는 첫 세대에서 사생활과 배터리 문제로 평가가 갈려 왔다. 출시 직후의 실제 사용기를 보고 판단해도 늦지 않다.

이 글은 2026년 11월 13일에 수집한 여러 매체의 기사 제목과 요약을 주제별로 묶어 정리한 것이다. 괄호 안은 보도한 매체이며, 기사 본문을 직접 확인한 내용이 아니므로 세부 사실은 각 매체의 원문을 확인하기 바란다. 확인되지 않은 개인 관련 의혹은 다루지 않았고, 특정 종목의 매수·매도를 권하는 글이 아니다.

웹앱에 음성 질문 기능 붙이기 — 브라우저 음성 인식 대신 멀티모달 모델로 받아쓰기

개인 대시보드에 "말로 물어보면 말로 답하는" 기능을 붙였다. 텍스트·이미지·음성을 한 모델이 다루는 멀티모달이 흔해지면서 이런 기능의 문턱이 많이 낮아졌다. 다만 만들고 보니 모델보다 브라우저와 기기 쪽에서 배운 것이 더 많았다.

구성

마이크 녹음 ──▶ 서버 ──▶ 받아쓰기(멀티모달 모델) ──▶ 답변 생성(가벼운 모델) ──▶ 음성 합성 ──▶ 재생

받아쓰기를 브라우저에 맡기지 않은 이유

브라우저에는 음성 인식 API 가 있다. 무료이고 코드도 짧다. 그런데 기기와 브라우저에 따라 지원 여부가 제각각이고, 앱 안에 내장된 브라우저에서는 아예 없는 경우가 많았다. 그래서 녹음한 오디오를 서버로 보내 멀티모달 모델에게 "이 음성을 받아 적어 줘"라고 시키는 방식을 택했다.

  • 어느 브라우저에서나 똑같이 동작한다. 필요한 것은 녹음 기능뿐이다.
  • 오디오를 요청 본문에 직접 실어 보내면 되므로 별도의 음성 인식 서비스를 붙일 필요가 없다.
  • 개발할 때 편하다. 녹음 파일을 코드로 직접 넣어 볼 수 있어서 마이크가 막힌 환경에서도 시험할 수 있었다.

답변은 가벼운 모델로

질문에 답할 때는 대시보드가 이미 모아 둔 데이터의 요약을 맥락으로 준다. 긴 추론이 필요 없는 일이라 가벼운 모델로 충분했고 1~2초면 답이 나왔다. 전체 대기 시간의 대부분은 오히려 마지막의 음성 합성이었다. 60자 정도의 답을 소리로 만드는 데 5초쯤 걸렸다. 그 사이의 침묵을 줄이려고 "확인해 볼게요" 같은 짧은 음성을 먼저 틀어 준다.

겪은 문제들

  • 말이 끝난 시점 알아내기. 녹음 버튼을 다시 누르게 하는 것은 불편해서, 소리 크기가 일정 시간 이상 작으면 끝난 것으로 본다. 기준값을 고정하면 시끄러운 곳에서는 영영 끝나지 않으므로, 시작할 때 잰 배경 소음의 몇 배를 기준으로 삼고 최솟값을 두었다.
  • 대기 음성이 답변을 지워 버렸다. 재생 목록에 이전 항목이 남은 상태에서 대기 음성을 틀었더니, 그 음성이 끝나면서 "다음 항목 재생"이 호출되고, 그 과정에서 재생 상태가 초기화돼 정작 도착한 답변이 버려졌다. 대기 음성을 틀기 전에 재생 목록을 먼저 비워야 했다. 비동기 재생이 얽힌 코드에서는 "누가 언제 상태를 초기화하는가"를 한 번은 그려 봐야 한다.
  • 서버에 본문 읽는 코드가 없었다. 조회만 하던 서버라 요청 본문을 읽는 부분이 아예 없었다. 오디오를 받으려고 추가하면서 크기 상한(몇 메가바이트)을 함께 넣었다. 상한 없는 업로드 경로는 그 자체로 약점이다.
  • 차에서 쓰면 "통화 중"이 된다. 가장 예상하지 못한 문제였다. 안드로이드의 브라우저에서 마이크를 여는 순간 운영체제가 블루투스를 통화용 연결로 전환한다. 차량 화면에 통화 표시가 뜨고 듣던 음악의 음질이 떨어진다. 마이크의 음성 처리 옵션을 모두 끄면 피할 수 있다는 기기도 있지만 확실한 방법은 찾지 못했다.

그래서 바꾼 것

  • 질문 입력의 기본을 글로 바꾸고, 마이크는 입력창 옆의 선택 기능으로 내렸다. 음성 입력이 필요하면 키보드 앱의 받아쓰기를 쓰는 것도 방법이다. 그쪽은 통화 모드로 전환하지 않는다.
  • 화면이 가려지면 녹음을 즉시 취소하고, 20초가 넘으면 강제로 끝낸다. 마이크가 켜진 채 남는 일은 없어야 한다.

정리

멀티모달 모델 덕분에 "음성을 받아 적고, 답하고, 읽어 주는" 흐름은 며칠이면 만든다. 시간이 드는 곳은 그 앞뒤다. 언제 말이 끝났는지, 소리가 어디로 나가는지, 기기가 마이크를 어떻게 취급하는지. 음성 기능은 모델 데모가 아니라 실제로 쓸 장소에서 시험해 봐야 한다.

예약 실행되는 AI 에이전트에게 일을 맡기는 요령 — 기억 없는 실행을 위한 지침 쓰기

대화창에서 시키는 에이전트와 달리, 정해진 시각에 혼자 깨어나 일하고 결과만 남기는 에이전트가 있다. 장중 점검, 주간 정리 같은 일을 몇 개 맡겨 보니, 잘 돌아가게 하는 요령은 대화형과 꽤 달랐다. 핵심은 하나다. 예약 실행은 매번 아무 기억 없이 시작한다.

지침은 혼자서 완결돼야 한다

대화 중에는 "아까 그 파일"이라고 해도 통한다. 예약 실행에는 "아까"가 없다. 지침서에 다음이 모두 들어 있어야 한다.

  • 어디서 일하는가: 작업 폴더, 접속할 서버, 읽을 파일의 정확한 경로.
  • 먼저 무엇을 읽는가: 프로젝트 메모, 규칙 문서. 지침서에 규칙을 전부 복사하지 말고 정본 파일을 가리킨다. 규칙을 고칠 때 한 곳만 고치면 된다.
  • 무엇을 하면 끝인가: 완료 조건. 없으면 일이 끝없이 커지거나 너무 일찍 끝난다.

금지 사항은 목록으로

사람이 지켜보지 않는 실행에서는 "해도 되는 일"보다 "하면 안 되는 일"을 구체적으로 적는 것이 중요했다.

  • 조회만 하는 작업이면 "설정 변경, 재시작, 주문, 삭제를 하지 않는다"를 명시한다.
  • 읽어도 되는 범위를 정한다. "이 폴더 밖은 수정하지 않는다", "비밀 값이 든 파일은 열지 않는다".
  • 검사를 우회하지 못하게 한다. 자동 검사에 걸렸을 때 에이전트가 택할 수 있는 가장 쉬운 길은 검사기를 고치는 것이다. "걸리면 결과물을 고친다. 검사 규칙은 건드리지 않는다"를 적어 둔다.

상태는 파일에 남긴다

지난번에 무엇을 했는지 알아야 하는 작업이라면 장부 파일을 둔다. 실행할 때마다 장부를 읽고, 끝나면 한 줄 추가한다. 이것이 예약 에이전트의 유일한 기억이다. 장부가 없으면 같은 일을 반복하거나, 같은 주제를 두 번 다룬다.

애매하면 멈추게 한다

사람이 옆에 있으면 물어보면 된다. 예약 실행에서는 물을 사람이 없으니 기본 행동을 정해 줘야 한다. 내 기준은 이렇다.

  • 되돌릴 수 있는 일은 진행하고 보고한다.
  • 되돌릴 수 없거나 밖으로 나가는 일은, 조금이라도 애매하면 하지 않고 "왜 하지 않았는지"를 보고한다.
  • 인증 만료 같은 오류는 재시도하지 않는다. 사람이 풀어야 하는 문제를 에이전트가 반복해서 두드리면 계정이 잠길 수 있다.

보고 형식을 정한다

결과 보고는 내가 30초 안에 읽을 수 있어야 한다. "무엇을 했는지 한 줄, 근거 한 줄, 하지 못한 것과 이유"로 형식을 정해 주면 매번 같은 자리에서 같은 정보를 찾을 수 있다. 민감한 값은 보고에 쓰지 말고 "어느 파일 몇 번째 줄"로만 가리키게 한다.

운영하면서 겪은 것

  • 첫 실행은 직접 지켜본다. 권한 승인이 필요한 도구가 있으면 첫 실행에서 멈춘다. 한 번 수동으로 돌려 승인해 두지 않으면 예약 시각에 조용히 멈춰 있다.
  • 기기가 꺼져 있으면 돌지 않는다. 내 PC 에서 도는 예약 작업은 그 시각에 앱이 꺼져 있으면 다음에 켤 때 실행된다. 시각이 중요한 작업이라면 서버에서 돌리는 방식을 택해야 한다.
  • 한도를 나눠 쓴다는 것을 기억한다. 예약 작업이 쓰는 API 한도는 내가 낮에 시험하며 쓰는 한도와 같은 주머니다. 낮에 시험을 많이 돌린 날 저녁 작업이 한도 초과로 실패한 적이 있다.
  • 한 번 쓰고 끝나는 작업은 일회성으로 등록한다. 반복 일정으로 걸어 두고 잊으면 의미 없는 실행이 계속 쌓인다.

정리

예약 에이전트의 지침서는 프롬프트라기보다 야간 근무자에게 남기는 인수인계서에 가깝다. 어디서 무엇을 보고, 무엇은 절대 건드리지 말고, 애매하면 어떻게 하고, 아침에 무엇을 보고할지. 그것만 잘 쓰면 모델의 성능 차이보다 훨씬 큰 차이가 났다.

프롬프트로 부탁하는 것과 출력을 검사하는 것은 다른 층이다 — LLM 가드레일 3층

LLM 에게 "이건 하지 마"라고 프롬프트에 적으면 대부분 지킨다. 문제는 "대부분"이다. 매일 자동으로 도는 기능에서 백 번에 한 번은 매주 한 번이다. 몇 가지 기능을 운영하면서, 지켜져야 하는 규칙은 프롬프트 한 곳에만 두지 않고 세 층으로 나눠 두게 됐다.

1층: 모델에게 주지 않는다

가장 확실한 방법은 새어 나가면 안 되는 정보를 애초에 프롬프트에 넣지 않는 것이다.

  • 개인 자산을 요약해 주는 기능에서 금액과 수량은 모델에게 보내지 않는다. 등락률 같은 비율만 보낸다. 모델은 모르는 것을 말할 수 없다.
  • 외부에 내보내는 응답은 "뺄 것"을 고르지 않고 "넣을 것"을 고른다. 내보낼 필드를 명시한 허용 목록으로 만들면, 나중에 내부 데이터에 민감한 필드가 추가돼도 자동으로 따라 나가지 않는다.

2층: 프롬프트로 요청한다

말투, 형식, 다루는 범위는 프롬프트의 영역이다. 여기서 배운 것들:

  • 중요한 지시는 맨 앞에. 추론을 꺼 둔 가벼운 모델은 긴 프롬프트의 뒤쪽 지시를 자주 흘렸다. 절대 어기면 안 되는 조건은 첫 문단에 둔다.
  • 이유와 상황을 준다. "내일은 쉬는 날이다"라는 사실을 데이터로 넘기는 것만으로는 부족했고, "그러니 출근 이야기를 하지 말 것"까지 써야 했다.
  • 지시문과 본문이 섞이는 경로를 조심한다. 음성 합성 모델에 "차분하게 읽어 줘"라는 지시를 본문 앞에 붙였더니 가끔 그 지시문까지 소리 내어 읽었다. 지시와 내용을 분리해 받을 수 없는 모델이라면 지시를 빼는 편이 낫다.

3층: 출력을 검사한다

프롬프트를 아무리 다듬어도 새는 것은 코드로 잡는다. 검사는 모델의 "의도"가 아니라 "결과물"을 본다는 점에서 2층과 성격이 다르다.

  • 형식 검증 후 재요청. 정해진 구조(예: 네 글자 한자와 뜻풀이)로 받아야 하는 출력은 코드로 형식을 확인하고, 틀리면 한 번 더 요청한다.
  • 금지 패턴이 든 문장만 고쳐 쓰기. 쉬는 날 전날의 브리핑 원고에 "내일 … 출근"이 있으면 그 문장만 가벼운 모델에게 다시 쓰게 한다. 전체를 다시 만드는 것보다 싸고, 다시 만든 원고가 또 틀릴 위험도 없다. 이 검사를 넣기 전에는 프롬프트를 두 번 고쳐도 새어 나왔고, 넣은 뒤로는 실측에서 위반이 없었다.
  • 문장 단위 삭제. 요약문에 손익이나 평균 단가를 언급한 문장이 있으면 그 문장을 통째로 지운 뒤 내보낸다. 1층에서 숫자를 주지 않았더라도 모델이 다른 단서로 추정해 쓰는 경우를 막는 마지막 그물이다.
  • 검사 자체를 테스트한다. 금지 값이 든 예문은 반드시 걸리고 정상 예문은 통과하는지를 자동 테스트로 묶어 둔다. 검사기가 조용히 고장 나면 세 층이 모두 뚫린다.

어느 층에 무엇을 둘까

  • 새어 나가면 되돌릴 수 없는 것(개인정보, 금액, 비밀 값) → 1층과 3층 양쪽에.
  • 틀리면 민망하지만 고칠 수 있는 것(말투, 길이, 순서) → 2층이면 충분.
  • 틀리면 기능이 깨지는 것(형식, 필수 항목) → 2층에 적고 3층에서 확인.

정리

프롬프트는 부탁이고, 검사는 보장이다. 부탁으로 충분한 일이 대부분이지만, 자동으로 돌아가며 사람이 매번 보지 않는 출력이라면 "이건 절대 안 된다"에 해당하는 규칙만큼은 코드로 한 번 더 걸러야 마음이 놓인다.

MCP 서버를 내 PC 에 설치하지 않고 쓰기 — 서버 컨테이너 + ssh 로 붙이는 방법

에이전트에 도구를 붙이는 표준으로 MCP(Model Context Protocol)가 자리를 잡으면서, 서비스 회사가 직접 MCP 서버를 내놓는 일이 늘었다. 그런데 설치 안내는 대개 "내 PC 에 런타임을 깔고 실행"이다. 내 PC 는 보안 정책 때문에 새 실행 파일이 막혀 있어서 그렇게 할 수 없었다. 결국 MCP 서버는 원격 서버의 컨테이너에서 돌리고, PC 의 에이전트는 ssh 로만 붙는 구성으로 풀었고, 써 보니 이쪽이 오히려 나았다.

원리

MCP 의 가장 단순한 연결 방식은 표준 입출력(stdio)이다. 에이전트가 명령 하나를 실행하고 그 프로세스와 입출력으로 대화한다. 여기서 "명령"이 꼭 로컬 프로그램일 필요는 없다. ssh 도 표준 입출력을 그대로 이어 주는 명령이다.

에이전트(PC) ──stdio──▶ ssh my-server run-mcp.sh ──▶ docker run -i ... (MCP 서버)

서버 쪽

  1. MCP 서버 저장소를 받아 이미지를 만든다.
  2. 실행 스크립트를 하나 둔다. 컨테이너를 -i(표준 입력 연결)로 띄우고, 끝나면 지워지게 --rm 을 준다.
    #!/bin/sh
    # ~/bin/run-mcp.sh
    exec docker run --rm -i \
      -e MCP_TRANSPORT=stdio \
      --env-file /home/user/mcp.env \
      my-mcp:local

내가 쓴 서버는 Dockerfile 기본값이 HTTP 방식이어서 stdio 로 바꾸는 환경 변수가 필요했다. 이런 것은 저장소의 설정 문서를 봐야 안다.

PC 쪽

에이전트의 MCP 설정에 ssh 명령을 등록한다.

ssh -o BatchMode=yes my-server bin/run-mcp.sh
  • BatchMode=yes 는 비밀번호를 묻는 프롬프트가 뜨지 않게 한다. 프롬프트가 뜨면 그 글자가 프로토콜에 섞여 연결이 깨진다. 키 인증이 되어 있어야 한다.
  • 서버의 로그인 셸이 접속할 때 환영 문구를 출력하면 역시 프로토콜이 깨진다. 비대화형 접속에서는 아무것도 찍지 않게 한다.

겪은 함정

  • 경로가 멋대로 바뀐다. Windows 의 Git Bash 에서 등록 명령을 실행하면서 서버 쪽 절대 경로(/home/user/bin/...)를 인자로 줬더니, 셸이 그것을 Windows 경로로 "친절하게" 바꿔서 저장했다. 상대 경로를 쓰거나, 변환을 끄는 환경 변수(MSYS_NO_PATHCONV=1)를 붙여야 한다.
  • 인증 토큰 충돌 가능성. 같은 API 키로 기존 앱과 MCP 서버가 각자 토큰을 발급받으면 서로의 토큰을 무효로 만드는 서비스가 있다. 앱 쪽에 "토큰 무효 오류면 재발급 후 한 번 재시도"를 넣어 두었다.

이 구성이 더 나았던 점

  • 비밀 값이 PC 에 없다. API 키는 서버의 설정 파일에만 있고, 컨테이너에 환경 변수로만 전달된다. 에이전트의 설정 파일에도, PC 디스크에도 남지 않는다.
  • 위험한 도구를 서버에서 잠근다. 내가 쓴 MCP 서버는 조회 도구와 실행 도구가 나뉘어 있고, 실행 도구는 환경 변수로 켜야만 등록됐다. 실행 스크립트에서 그 값을 "꺼짐"으로 고정해 두면 에이전트가 무슨 말을 듣든 그 도구는 존재하지 않는다. 프롬프트로 "하지 마"라고 하는 것과는 차원이 다른 안전장치다.
  • 호출 IP 가 고정된다. IP 를 등록해야 하는 API 는 PC 에서 부르면 장소를 옮길 때마다 막힌다. 서버에서 부르면 늘 같은 IP 다.
  • 여러 계정을 나눠 붙일 수 있다. 스크립트 인자로 설정을 바꿔, 같은 서버를 이름만 다르게 여러 개 등록했다.

정리

MCP 서버를 꼭 에이전트와 같은 기기에서 돌릴 필요는 없다. stdio 를 ssh 로 늘이기만 하면 설치 제약을 피할 수 있고, 키와 권한을 서버 한 곳에서 관리하게 되는 덤이 따라온다.

LLM 에게 결정을 맡기지 않는 설계 — 돈이 걸린 개인 도구에 AI 를 붙이며 정한 선

개인 자산을 들여다보는 도구를 만들면서 LLM 을 붙였다. 뉴스와 공시를 요약하고, 내가 정해 둔 조건이 충족됐는지 알려 주는 용도다. 만들기 전에 가장 오래 고민한 것은 기능이 아니라 "AI 가 어디까지 하게 둘 것인가"였다. 그때 그은 선과 그것을 코드로 지키는 방법을 적는다.

선 1. 값은 사람이 정한다

"몇 퍼센트 오르면 알림" 같은 기준값을 LLM 이 제안하게 만들 수도 있었다. 그럴듯한 숫자를 그럴듯한 이유와 함께 내놓을 것이다. 바로 그래서 막았다. 근거 없이 그럴듯한 숫자는 위험하고, 결과에 대한 책임은 어차피 내가 진다.

  • 새 규칙을 만드는 화면의 입력 칸은 비워 둔다. 예시 숫자를 흐리게 보여 주는 것조차 넣지 않았다. 예시는 곧 추천처럼 읽힌다.
  • 널리 쓰이는 관례값(지표의 표준 설정 등)으로 미리 만들어 둔 규칙에는 이름에 "(관례값)"이라고 붙여, 내 판단이 아니라는 것을 드러냈다.
  • 대신 비교할 도구를 준다. 값의 조합별로 과거 데이터에 적용한 결과를 표로 보여 주고, 고르는 것은 사람이 한다.

선 2. 요약은 사실만

브리핑을 쓰는 모델의 지시문에 "사실 요약만. 매수·매도 권고, 전망, 목표 가격 금지"를 넣었다. 그리고 프롬프트를 믿는 데서 끝내지 않고, 생성된 글에서 손익이나 평균 단가를 언급한 문장은 내보내기 전에 코드로 지운다.

선 3. 민감한 숫자는 모델에게 주지 않는다

보유 수량, 평균 단가, 금액은 메모리와 로컬 저장소에만 두고 프롬프트에 넣지 않는다. 모델에는 등락률 같은 비율만 간다. 외부 API 로 나가는 데이터를 줄이는 것이기도 하고, 모델이 그 숫자를 요약문에 흘릴 가능성을 원천적으로 없애는 것이기도 하다. 저장소, 문서, 로그에도 같은 원칙을 적용했다.

선 4. 실행 권한은 코드에 없다

이 도구는 조회만 한다. 주문을 내는 API 호출은 클라이언트 코드에 아예 구현하지 않았다. "주문하지 말 것"이라는 지시는 뚫릴 수 있지만 존재하지 않는 함수는 호출할 수 없다.

자동 매매를 실험하는 별도의 연습용 봇은 모의 투자 서버만 쓰는데, 접속 주소를 설정이 아니라 코드에 고정하고 요청마다 그 주소가 맞는지 확인한다. 설정 실수 한 번으로 실제 계좌에 주문이 나가는 경로를 없앤 것이다. 키의 이름도 실제용과 다르게 지어 섞일 수 없게 했다.

선 5. 모르는 것은 모른다고 표시한다

  • 근사치로 계산한 값에는 화면에 "근사"라고 쓴다.
  • 과거 데이터가 없어 검증할 수 없는 조건이 규칙에 들어 있으면, 과거 검증 결과 화면에 "이 조건은 과거에는 항상 미충족으로 처리됨"이라는 경고를 띄운다. 경고가 없으면 결과 표가 그 조건까지 검증한 것처럼 보인다.
  • 데이터가 늦게 반영되는 시간대에는 "미반영" 표시를 붙인다. 틀린 숫자를 자신 있게 보여 주는 것이 가장 나쁘다.

정리

LLM 은 정리하고 설명하는 데 뛰어나다. 그래서 그 역할만 맡겼다. 기준을 정하는 일, 실행하는 일, 민감한 숫자를 아는 일은 구조적으로 떼어 놓았다. "AI 가 실수하지 않게 잘 시킨다"가 아니라 "실수해도 닿을 수 없게 만든다"가 돈이 걸린 도구에서의 기본값이라고 생각한다. 이 글은 설계 경험을 적은 것이며 투자에 관한 조언이 아니다.

팀에서 LLM API 를 쓸 때 키를 나눠 주지 말고 게이트웨이를 두자

팀에서 LLM API 를 쓰기 시작하면 가장 먼저 생기는 문제는 모델 선택이 아니라 키 관리다. 벤더 키를 메신저로 돌리고, 각자 PC 의 설정 파일에 넣고, 누가 얼마를 썼는지 아무도 모른다. 작은 중계 서버(게이트웨이) 하나로 이 문제를 정리한 경험을 적는다.

하는 일

각자의 도구 ──(게이트웨이 토큰)──▶ 게이트웨이 ──(벤더 키)──▶ LLM API
                                      │
                             사용량·비용 기록, 상한 검사
  • 벤더 키는 게이트웨이 서버에만 둔다. 사용자에게는 게이트웨이가 발급한 토큰을 준다. 토큰은 언제든 개별로 끊을 수 있고, 벤더 키는 한 번도 서버 밖으로 나가지 않는다.
  • 요청은 손대지 않고 그대로 넘긴다. 경로 앞부분으로 벤더를 고르고 나머지는 그대로 전달한다. 스트리밍 응답도 그대로 흘려보낸다. 게이트웨이가 요청 형식을 해석하지 않으니 벤더가 새 기능을 내도 고칠 것이 없다.
  • 토큰마다 라벨과 일일 상한을 둔다. "이 토큰은 하루 얼마까지". 실수로 반복 호출을 걸어도 피해가 상한에서 멈춘다.
  • 응답의 사용량 정보를 읽어 비용을 기록한다. 누가, 어떤 모델로, 얼마를 썼는지가 남는다.

클라이언트는 한 줄만 바꾼다

공식 SDK 는 대부분 접속 주소를 바꿀 수 있다. 주소를 게이트웨이로, 키 자리에 게이트웨이 토큰을 넣으면 나머지 코드는 그대로다.

const client = new Anthropic({
  baseURL: 'https://llm.example.com/anthropic',
  apiKey: process.env.GATEWAY_TOKEN,
});

만들면서 겪은 것

  • 비용 집계는 사고 토큰을 따로 더해야 맞는다. 벤더에 따라 사용량 정보에서 답변 토큰과 사고 토큰이 다른 필드로 온다. 하나만 보면 비용이 실제의 몇 분의 일로 찍힌다.
  • 단가표에 없는 모델은 최고 단가로. 단가표는 수동 관리라 새 모델에 뒤처진다. 0원으로 계산하면 일일 상한이 조용히 무력해진다.
  • 상태 파일이 조용히 안 써졌다. 컨테이너를 루트가 아닌 사용자로 돌리는데, 이름 있는 볼륨은 루트 소유로 만들어진다. 사용량 기록이 권한 오류로 실패하고 있었는데 오류가 겉으로 드러나지 않았다. 이미지를 만들 때 그 디렉터리를 미리 만들고 소유자를 바꿔 두면 새 볼륨이 그 소유권을 물려받는다. 이미 만들어진 볼륨은 지우고 다시 만들어야 했다.
    RUN mkdir -p /state && chown node:node /state
    USER node
  • 오류 응답에 내부 정보를 싣지 않는다. 상태 확인 주소와 오류 메시지에는 어떤 벤더와 연결돼 있는지, 키가 어떤 상태인지를 넣지 않았다.

게이트웨이가 풀어 주지 않는 문제

프롬프트에 담긴 내용은 결국 외부 서비스로 나간다. 게이트웨이는 누가 얼마나 보냈는지는 알려 주지만, 보내도 되는 내용인지는 판단해 주지 않는다. 사내 코드나 고객 데이터를 다루는 도구를 붙일 때는 그 저장소와 데이터가 외부 전송이 허용된 것인지를 먼저 확인해야 한다. 우리는 코드 원문 대신 구조 요약(함수와 선언의 이름)만 보내는 모드를 따로 만들었는데, 비용 절감 효과는 거의 없었고 오직 이 목적으로만 쓸모가 있었다. 그리고 이런 도구의 도입 자체는 조직의 보안 정책 안에서 합의하고 진행할 일이다.

정리

게이트웨이는 백 줄 남짓한 프록시로 시작할 수 있다. 키가 한 곳에만 있고, 사람마다 상한이 있고, 사용량이 보인다. 이 세 가지만으로도 "누가 키를 깃에 올렸다"와 "이번 달 청구서가 왜 이래"라는 두 가지 사고를 예방한다.

입력을 4분의 1로 줄였는데 LLM 비용이 그대로였다 — 사고 토큰과 비용 계측

LLM API 비용을 줄이려고 프롬프트를 열심히 다이어트했는데 청구액이 거의 그대로였다. 원인을 따라가 보니 돈은 입력이 아니라 출력, 그중에서도 눈에 보이지 않는 사고(thinking) 토큰에서 나가고 있었다.

실측

코드 변경 내용을 읽고 문서를 써 주는 작업이었다. 원문을 통째로 보내는 대신 요약해서 보내는 모드를 만들어 입력을 약 4분의 1로 줄였다. 결과는 문서 한 건당 비용이 4% 남짓 줄어든 것이 전부였다.

사용량을 뜯어보니 이랬다.

  • 비용의 약 80%가 출력 쪽이었다. 출력 토큰의 단가는 입력보다 몇 배 비싸다.
  • 그 출력의 절반가량이 사고 토큰이었다. 모델이 답을 쓰기 전에 속으로 추론하는 데 쓰는 토큰인데, 응답 본문에는 보이지 않지만 출력 단가로 과금된다.

입력을 줄이는 최적화는 비용 구조의 20% 안에서만 움직인 셈이다. 그 요약 모드는 결국 비용이 아니라 "외부로 나가는 원문을 줄인다"는 다른 목적으로만 의미가 있었다.

계측에서 놓치기 쉬운 것

  • 사고 토큰은 다른 필드에 있다. 어떤 API 는 응답의 사용량 정보에서 답변 토큰과 사고 토큰을 따로 준다. 답변 토큰만 더하면 비용이 크게 과소 계산된다. 짧은 답 하나에 사고 119 토큰, 답 5 토큰이 나온 적도 있다. 스무 배가 넘는 차이다.
  • 단가표에 없는 모델을 0원으로 치면 안 된다. 직접 만든 사용량 집계에 모델별 단가표를 넣어 뒀는데, 새 모델로 바꾸면서 표를 갱신하지 않았다. 집계는 그 모델의 비용을 0으로 계산했고, 걸어 둔 일일 예산 상한은 아무 경고 없이 뚫렸다. 지금은 표에 없는 모델을 가장 비싼 단가로 계산하고 로그에 추정치 표시를 붙인다. 모르는 값은 안전한 쪽으로 틀려야 한다.
  • 구독과 API 는 지갑이 다르다. 채팅 앱이나 코딩 도구의 월 구독은 API 사용료를 포함하지 않는다. 구독 중이니 API 도 되겠지 하고 붙였다가 잔액 부족 오류를 만난다.

그래서 어디를 줄여야 하나

  1. 먼저 잰다. 호출마다 입력·출력·사고 토큰을 따로 기록한다. 어디가 큰지 모르고 줄이는 것은 운에 맡기는 일이다.
  2. 사고를 조절한다. 분류, 번역, 한 줄 요약처럼 추론이 필요 없는 작업은 사고를 끄거나 예산을 낮춘다. 내 경우 가벼운 모델이 짧은 답 하나에 천 토큰 넘게 생각하고 있었고, 끄자 속도와 비용이 함께 떨어졌다.
  3. 출력 길이를 설계한다. "자세히 설명해 줘"는 비싼 요청이다. 필요한 필드만 구조화해서 받으면 출력이 줄고 뒤처리도 쉬워진다.
  4. 자주 도는 호출부터 본다. 하루 한 번 도는 긴 작업보다 5분마다 도는 사소한 작업이 총액을 지배하는 경우가 많다. 그런 호출은 가벼운 모델로 내리고 결과를 캐시한다.
  5. 입력 최적화는 마지막에. 반복되는 긴 지시문이 있다면 프롬프트 캐싱이 효과적이지만, 위의 것들을 먼저 한 뒤의 이야기다.

정리

LLM 비용은 직관과 다르게 움직인다. "보내는 글이 길어서 비싸다"는 느낌은 대개 틀렸고, 보이지 않는 사고 토큰과 자주 도는 호출이 진짜 범인이었다. 줄이기 전에 재고, 모르는 단가는 비싸게 잡는다.

AI 코딩 에이전트의 셸 도구가 내 명령을 바꿔 버릴 때 — 백슬래시, 길이 제한, 인코딩

코딩 에이전트는 셸 명령을 실행하는 도구를 통해 파일을 고치고 서버를 다룬다. 그런데 에이전트가 "실행하려던 명령"과 "실제로 실행된 명령"이 다를 때가 있다. 에이전트는 자기가 보낸 내용이 그대로 실행됐다고 믿기 때문에, 이런 문제는 엉뚱한 곳을 한참 디버깅하게 만든다. 내 환경(Windows 에서 유닉스 계열 셸을 쓰는 구성)에서 겪은 것들을 모았다.

1. 백슬래시가 줄어든다

에이전트가 셸에서 한 줄짜리 스크립트로 파일을 고치게 했더니, 정규식의 \\d 가 \d 로, 경로의 이중 백슬래시가 단일 백슬래시로 바뀌어 저장됐다. 명령이 도구를 거치고 셸을 거치면서 이스케이프가 한 겹씩 벗겨진 것이다. 설정 파일의 경로가 깨지고 정규식이 엉뚱한 것을 찾았다.

대책: 백슬래시가 들어가는 내용은 셸 명령에 싣지 않는다. 에이전트의 파일 쓰기·편집 도구로 직접 쓰게 하거나, 스크립트를 파일로 저장한 뒤 그 파일을 실행하게 한다.

2. 긴 명령은 잘린다

한 번에 보낸 명령이 10KB 를 넘자 뒷부분이 잘린 채 실행됐다. 긴 패치를 셸 명령 하나에 통째로 넣는 방식에서 생겼다. 오류가 나면 다행이고, 잘린 지점이 하필 문법상 유효한 곳이면 절반만 적용된 채 "성공"한다.

대책: 긴 내용은 파일로 쓰고 실행은 짧은 명령으로. 적용 뒤에는 기대한 문자열이 실제로 파일에 있는지 확인하게 한다.

3. 안전 검사가 정상 명령을 막는다

달러 기호가 든 여러 줄 입력(heredoc)이 도구의 안전 검사에 걸려 거부된 적이 있다. 변수 치환을 이용한 위험한 명령과 구분하기 어렵기 때문일 것이다. 에이전트는 표현을 바꿔 가며 여러 번 재시도하다 시간을 썼다.

대책: 이것도 답은 같다. 스크립트를 파일로 만들고 그 파일을 실행한다.

4. 한글이 깨진다

한글이 든 JSON 을 명령줄 인자로 넘겨 API 에 보냈더니 서버에 깨진 글자가 저장됐다. Windows 에서는 명령줄 인자가 프로세스로 넘어가며 코드 페이지 변환을 거친다. 영문만 보낼 때는 멀쩡해서 한참 뒤에 발견했다.

대책: 본문은 UTF-8 파일로 저장하고 파일에서 읽어 보내게 한다.

5. 줄 끝이 바뀐다

Windows 에서 만든 스크립트를 리눅스 서버에 올리면 줄 끝이 CRLF 일 수 있다. 직접 실행하면 얼추 돌다가 예약 실행에서만 "찾을 수 없음"으로 조용히 실패한다.

대책: 저장소에 줄 끝 규칙을 고정한다(* text=auto eol=lf).

6. 원격 명령이 자기 자신을 죽인다

에이전트가 원격 서버에 "이 이름이 들어간 프로세스를 종료"하는 명령을 한 줄로 보냈더니 접속이 끊겼다. 명령줄 전체에서 이름을 찾는 옵션을 쓴 탓에, 그 명령을 실행하던 원격 셸 자신도 대상이 된 것이다.

대책: 전체 일치 옵션을 쓰거나 프로세스 번호로 종료한다.

7. 고쳤는데 그대로다

에이전트가 코드를 고치고 "강제 새로 고침"으로 확인했는데 옛 동작이 그대로였다. 개발 서버가 고치기 전 모듈을 메모리에 들고 있었다. 에이전트는 수정이 반영됐다고 가정하고 다른 원인을 찾아 헤맸다.

대책: 서버 쪽 코드를 고친 뒤에는 프로세스를 재시작하고 확인하는 것을 규칙으로 적어 둔다.

공통된 교훈

  • "보낸 것"이 아니라 "남은 것"을 확인하게 한다. 편집 뒤에는 파일을 다시 읽거나 검색해서 실제 내용을 본다. 이 한 단계가 위 문제의 대부분을 일찍 잡는다.
  • 내용은 파일로, 명령은 짧게. 셸을 통과하는 문자열이 짧고 단순할수록 변형될 여지가 없다.
  • 겪은 함정은 프로젝트 메모에 적는다. 이 글의 항목 대부분은 지금 내 프로젝트 메모의 맨 위에 있고, 그 뒤로는 같은 문제로 시간을 쓰지 않았다. 에이전트는 자기 도구의 한계를 스스로 기억하지 못한다.

정리

에이전트가 이상한 버그를 쫓고 있다면, 코드보다 먼저 "에이전트의 명령이 온전히 전달됐는가"를 의심해 볼 만하다. 도구와 셸 사이에서 조용히 바뀐 한 글자가 원인인 경우가 생각보다 많았다.

AI 코딩 에이전트가 같은 실수를 반복하지 않게 하는 법 — 프로젝트 메모 파일 운영기

코딩 에이전트는 똑똑하지만 어제 일을 기억하지 못한다. 새 대화를 열 때마다 같은 함정에 다시 빠지고, 같은 구조를 다시 탐색한다. 몇 달 동안 여러 프로젝트를 에이전트와 함께 굴리면서 가장 효과가 컸던 것은 모델을 바꾸는 일이 아니라 프로젝트 메모 파일을 가꾸는 일이었다.

메모 파일이란

요즘 코딩 에이전트는 대부분 저장소 안의 지침 파일을 대화 시작 때 읽는다. 이름은 도구마다 다르지만 역할은 같다. "이 프로젝트에서 일하려면 알아야 할 것"을 적어 두는 곳이다. 여기에 무엇을 어떻게 적느냐에 따라 에이전트의 쓸모가 크게 달라졌다.

효과가 있었던 규칙

  • 코드를 보면 알 수 있는 것은 적지 않는다. 폴더 구조나 함수 목록은 에이전트가 직접 읽으면 된다. 적어야 하는 것은 코드에 없는 것, 곧 "왜 이렇게 했는가"와 "이렇게 하면 안 된다"이다.
  • 함정은 겪은 즉시 한 줄로. 문제를 해결한 직후 그 자리에서 에이전트에게 "방금 알게 된 것을 메모에 추가해"라고 시킨다. 눈에 띄는 표시를 붙여 두면 나중에 훑어보기 쉽다. 형식은 "증상 → 원인 → 이렇게 한다".
  • 확인한 사실과 추측을 구분한다. "(실측)", "(추정)" 같은 꼬리표를 단다. 추측이 꼬리표 없이 들어가면 다음 대화의 에이전트는 그것을 사실로 믿고 그 위에 일을 쌓는다. 실제로 틀린 추측 한 줄 때문에 서비스 중단을 예상하지 못한 적이 있다.
  • 사람의 결정을 적는다. "이 값은 내가 정한다, 추천하지 말 것", "발행은 사람이 직접" 같은 결정은 코드에서 읽어 낼 수 없다. 적어 두지 않으면 에이전트는 매번 친절하게 같은 제안을 다시 한다.
  • 파일을 쪼갠다. 메모가 길어지면 모든 대화가 그 전부를 읽느라 느려지고, 관련 없는 내용이 섞여 판단이 흐려진다. 맨 위 파일에는 어디에나 해당하는 규칙만 두고, 나머지는 영역별·프로젝트별 파일로 내린다. 소유 경계(개인 것, 회사 것, 고객 것)로 나누니 "이 정보를 저쪽 산출물에 쓰면 안 된다"는 규칙도 자연스럽게 지켜졌다.
  • "다시 찾아보지 말 것"을 적는다. 배포 절차나 서버 접속 방법처럼 반복되는 일은 메모가 정본이라고 못박는다. 그러지 않으면 에이전트는 성실하게 매번 처음부터 조사한다.

적지 말아야 할 것

  • 비밀 값. 키, 토큰, 비밀번호는 절대 넣지 않는다. 메모 파일은 대화마다 모델에 통째로 전달되고, 저장소에 커밋될 수도 있다. "어느 파일에 있다"까지만 적는다.
  • 끝난 일의 기록. "몇 월 며칠에 무엇을 고쳤다"는 버전 관리 기록에 있다. 메모에는 그 일에서 배운 것만 남긴다.
  • 지금만 맞는 상태. "현재 작업 중" 같은 문장은 금방 거짓이 된다.

정리

에이전트와 일하는 것은 매일 아침 기억을 잃는 유능한 동료와 일하는 것과 비슷하다. 그 동료가 출근해서 가장 먼저 읽는 쪽지를 잘 써 두는 것이 프롬프트를 다듬는 것보다 훨씬 오래 남는 투자였다.

AI 에이전트에게 웹 관리 화면을 맡길 때 — 클릭 자동화보다 세션 API 호출이 나았던 이유

브라우저를 직접 조작하는 AI 에이전트를 써서 서버 관리 화면의 반복 작업을 맡겨 봤다. 스택 재배포, 인증 설정 추가, DNS 레코드 등록 같은 일이다. 화면을 보고 클릭하는 방식은 데모에서는 근사하지만 실제 관리 콘솔에서는 자주 미끄러졌다. 결국 자리 잡은 방식과 그 한계를 정리한다.

화면 클릭이 깨지는 곳

  • 예고 없는 팝업. 유료 기능 안내, 새 버전 소개, 설문 같은 것이 버튼 위를 덮는다. 에이전트는 방금 본 화면의 좌표를 누르는데 그 자리에 다른 것이 와 있다.
  • 여러 단계 마법사. 단계마다 입력 요소가 동적으로 바뀌는 설정 마법사에서 자동 입력이 값을 놓치거나 검증 오류를 냈다.
  • 레이아웃 이동. 값을 입력하자 안내 문구가 두 줄이 되면서 저장 버튼이 20픽셀 내려간다. 이전 화면 기준으로 누른 클릭은 빗나간다.
  • 맞춤 드롭다운. 표준 입력 요소가 아니라 직접 그린 목록은 키보드 조작이 한 번은 되고 한 번은 안 됐다.

더 나았던 방식: 로그인된 세션으로 API 호출

대부분의 관리 콘솔은 화면이 자기 REST API 를 부르는 구조다. 사람이 이미 로그인해 둔 브라우저 탭 안에서 그 API 를 직접 부르면, 화면 배치와 무관하게 같은 일을 할 수 있다.

const res = await fetch('/api/stacks/42/redeploy', {
  method: 'PUT',
  credentials: 'include',                       // 로그인 쿠키 사용
  headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
  body: JSON.stringify({ pullImage: false }),
});

붙이면서 알게 된 것들:

  • CSRF 토큰이 필요한 경우가 많다. 어떤 콘솔은 상태 조회 응답의 헤더에 토큰을 실어 주고, 어떤 것은 쿠키 값을 그대로 헤더에 넣으라고 한다. 브라우저 개발자 도구의 네트워크 탭에서 화면이 보내는 요청을 한 번 보면 알 수 있다.
  • 인증 방식은 버전에 따라 바뀐다. 저장소에 토큰을 두던 콘솔이 업데이트 뒤 쿠키 세션으로 바뀌어, 잘 돌던 스크립트가 어느 날 인증 오류를 냈다.
  • API 로 만든 객체는 화면으로 만든 것과 다를 수 있다. 화면은 기본값을 채워 주지만 API 는 보낸 것만 저장한다. 필수처럼 보이지 않던 필드가 비어 있어 기능이 동작하지 않은 적이 있다. 화면으로 만든 정상 객체를 조회해서 비교하는 것이 가장 빨랐다.

이 방식이 막히는 곳

모든 서비스에서 통하지는 않는다. 어떤 대형 서비스의 대시보드는 세션으로 조회 요청은 받아 주지만 변경 요청은 보안 계층이 차단했다. 헤더를 맞춰도 마찬가지였다. 이런 곳은 화면 클릭으로 돌아가되, 한 동작마다 화면을 다시 확인하며 천천히 진행하는 수밖에 없다. 공식 API 토큰을 발급받을 수 있다면 그것이 정석이다.

사람이 해야 하는 일

  • 로그인. 비밀번호와 인증 코드는 사람이 직접 넣는다. 에이전트는 이미 열린 세션만 쓴다. 세션이 만료되면 사람에게 다시 로그인해 달라고 요청하게 한다.
  • 약관 동의와 결제. 버튼 하나로 계약이 성립하는 단계는 에이전트가 화면을 준비해 두고 사람이 누른다.
  • 되돌릴 수 없는 삭제. 실행 전에 대상과 영향을 사람이 확인한다.
  • 비밀 값 발급. 생성 직후 한 번만 보여 주는 키는 사람이 내려받아 정해진 위치에 둔다. 대화 내용에 값이 남지 않게 한다.

기록해 두면 다음이 빨라진다

어느 콘솔에서 어떤 방식이 통했는지를 프로젝트 메모에 남긴다. "이 콘솔은 화면 자동화가 팝업 때문에 깨지니 세션 API 로", "여기는 변경 요청이 막히니 화면으로". 이 한 줄이 없으면 다음 대화의 에이전트는 같은 시행착오를 처음부터 다시 한다.

정리

브라우저 에이전트의 강점은 "사람의 로그인 상태를 빌려 쓸 수 있다"는 것이지 "사람처럼 클릭한다"가 아니었다. 클릭은 최후의 수단으로 두고, 가능하면 화면 뒤의 API 를 부르게 하는 편이 빠르고 정확했다.

내 일을 대신 지켜보는 알림 봇 만들기 (0) 전체 구성

확인하러 들어가야만 알 수 있는 것들이 있다. 판매자 센터의 새 주문, 답변을 기다리는 문의, 아침에 챙겨 볼 뉴스, 자동화 스크립트가 방금 한 일. 이런 것을 대신 지켜보다가 채팅방에 한 줄 올려 주는 봇을 몇 개 만들어 쓰고 있다. 만드는 방법을 몇 편으로 나눠 적는다. 각 편은 알림봇-따라하기 라벨에 모여 있다.

알림 봇의 공통 구조

무엇을 알리든 뼈대는 같다.

주기 실행 ──▶ 외부 API 조회 ──▶ "새 것"만 추리기 ──▶ 채팅방에 게시 ──▶ (선택) 메신저로 사본
                                    ▲
                              상태 파일(어디까지 봤는지)
  • 주기 실행: 20분마다, 또는 매일 정해진 시각에.
  • 새 것만 추리기: 봇의 품질은 여기서 갈린다. 같은 알림이 두 번 오면 금방 무시하게 되고, 한 번 빠뜨리면 믿지 못하게 된다.
  • 게시: 직접 운영하는 팀 채팅 서버에 봇 계정으로 올린다. 알림 종류마다 채널을 나누면 나중에 찾아보기 쉽다.
  • 사본: 채팅 앱을 자주 열지 않는 사람을 위해, 중요한 알림 몇 가지는 평소 쓰는 메신저로도 보낸다.

편별 내용

  1. 채팅방에 봇으로 글 올리기 — 봇 토큰과 REST API, 채널 찾기, 표가 깨지는 이유, 매일 정시에 실행하는 루프
  2. 새 것만 알리기 — 상태 파일과 중복 키, 조회 구간 다루기, 그리고 오픈마켓 판매자 API 세 곳에서 겪은 것들
  3. 메신저로 사본 보내기 — 카카오톡 "나에게 보내기" 중계 서버, 토큰 갱신, 글자 수 제한과 묶음 전송

만들면서 정한 원칙

  • 알림은 한 줄로 끝나야 한다. 무슨 일인지, 어디서 확인하는지(링크). 요약이나 통계는 알림이 아니라 대시보드에 둔다.
  • 알림 경로가 죽어도 본업은 계속된다. 채팅 서버가 내려가 있어도 주문 조회 상태는 정상적으로 다음으로 넘어가야 하고, 사본 전송이 실패해도 원래 알림에는 영향이 없어야 한다.
  • 실패는 조용히 넘기되 흔적은 남긴다. 알림 봇이 스스로 알림 폭탄을 만들면 안 된다. 오류는 로그와 상태 확인 주소에 남기고, 다음 주기에 다시 시도한다.
  • 시험 전송 옵션을 둔다. 실제로 보내지 않고 "보냈을 내용"만 출력하는 --dry-run 이 있으면 고칠 때마다 채팅방을 어지럽히지 않는다.

봇을 돌릴 서버와 배포 방법은 홈서버-따라하기 시리즈에, 알림을 모아 한 화면에서 보는 방법은 대시보드-따라하기 시리즈에 있다.

내 일을 대신 지켜보는 알림 봇 만들기 (1) 채팅방에 봇으로 글 올리기

알림봇-따라하기 시리즈 1편. 알림의 종착지인 채팅방에 글을 올리는 부분부터 만든다. 나는 직접 운영하는 Mattermost 를 쓰지만, 봇 토큰으로 REST API 를 부르는 방식은 다른 채팅 도구도 비슷하다.

웹훅 대신 봇 계정

수신 웹훅(Incoming Webhook)이 가장 간단하지만, 나는 봇 계정과 액세스 토큰을 쓴다. 토큰 하나로 여러 채널에 쓸 수 있고, 나중에 채널의 글을 읽어 오는 기능(다른 봇의 알림을 대시보드에 모으기)에도 같은 토큰을 쓸 수 있기 때문이다. 봇을 만든 뒤 글을 올릴 채널에 봇을 멤버로 추가해야 한다. 빠뜨리면 403 이 온다.

게시 모듈

// lib/chat.js — 설정이 없으면 조용히 꺼진다
function createChat(env) {
  const base = (env.CHAT_URL || '').replace(/\/$/, '');
  const token = env.CHAT_TOKEN || '';
  const team = env.CHAT_TEAM || '', name = env.CHAT_CHANNEL_NAME || '';
  let channel = env.CHAT_CHANNEL || '';
  const enabled = !!(base && token && (channel || (team && name)));
  const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };

  async function channelId() {
    if (channel) return channel;
    const url = `${base}/api/v4/teams/name/${encodeURIComponent(team)}/channels/name/${encodeURIComponent(name)}`;
    const res = await fetch(url, { headers, signal: AbortSignal.timeout(15e3) });
    if (!res.ok) throw new Error(`채널 조회 ${res.status}`);
    channel = (await res.json()).id;
    return channel;
  }

  async function post(message) {
    if (!enabled) return false;
    const res = await fetch(`${base}/api/v4/posts`, {
      method: 'POST', headers, signal: AbortSignal.timeout(15e3),
      body: JSON.stringify({ channel_id: await channelId(), message }),
    });
    if (!res.ok) throw new Error(`게시 ${res.status}`);
    return true;
  }
  return { enabled, post };
}
module.exports = { createChat };
  • 채널은 이름으로 지정할 수 있게 했다. API 는 채널 ID 를 요구하는데, ID 는 화면에서 찾기 번거롭다. 주소창에 보이는 팀 이름과 채널 이름을 설정에 넣으면 처음 한 번 ID 를 조회해 기억한다.
  • 설정이 비어 있으면 꺼진다. 로컬에서 개발할 때 토큰을 넣지 않으면 알림만 빠지고 나머지는 그대로 돈다. 개발 중인 봇이 운영 채널에 글을 올리는 사고를 막는다.
  • 타임아웃은 필수. 채팅 서버가 응답하지 않을 때 봇 전체가 멈추면 안 된다.

같은 서버 안에서는 내부 주소로

봇과 채팅 서버가 같은 호스트의 다른 컨테이너라면 공개 주소로 돌아 나갔다 들어올 필요가 없다. compose 에 호스트 게이트웨이를 등록하고 내부 포트로 바로 부른다.

services:
  bot:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      CHAT_URL: http://host.docker.internal:8065

이렇게 바꾼 계기가 있다. 공개 주소 앞단의 보안 설정이 일부 HTTP 클라이언트의 User-Agent 를 봇으로 보고 403 으로 막았다. 내부 주소로 가면 그 계층을 거치지 않는다.

겪은 함정

  • 표가 표로 안 보인다. 마크다운 표는 바로 앞에 빈 줄이 있어야 표로 그려진다. 제목 줄 바로 다음 줄에 표를 붙이면 세로 막대가 그대로 보인다. 또 구분 줄에 정렬용 콜론(--:)을 넣으면 내 서버 버전에서는 표가 깨졌다. 단순한 --- 만 쓴다.
  • 컨테이너에 들어가 시험할 때 토큰이 없다. 서버가 시작할 때 설정 파일을 읽어 환경 변수를 채우는 구조라면, docker exec 로 따로 띄운 스크립트에는 그 값이 없다. 이것을 모르고 "토큰이 만료됐나?" 하며 401 의 원인을 엉뚱한 데서 찾았다. 시험 스크립트 첫 줄에서 같은 설정 파일을 읽게 한다: process.loadEnvFile('/config/.env')
  • 한글이 든 본문을 curl 로 시험할 때는 본문을 파일로 저장해 --data-binary @파일 로 보낸다. 명령줄에 직접 쓰면 환경에 따라 인코딩이 깨진다.

매일 정해진 시각에 실행하기

"매일 아침 8시에 뉴스 요약" 같은 알림은 cron 없이 프로세스 안에서 처리한다. 컨테이너 하나로 끝나고, 시각을 환경 변수로 바꿀 수 있다.

// AT="08:00,14:00" 처럼 여러 시각을 받는다
function nextRun(times, now = new Date()) {
  const candidates = times.flatMap((hhmm) => {
    const [h, m] = hhmm.split(':').map(Number);
    const today = new Date(now); today.setHours(h, m, 0, 0);
    const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1);
    return [today, tomorrow];
  });
  return candidates.filter((d) => d > now).sort((a, b) => a - b)[0];
}

async function loop() {
  for (;;) {
    const at = nextRun(TIMES);
    await new Promise((r) => setTimeout(r, at - Date.now()));
    await runOnce().catch((e) => console.error(e.message));
  }
}
  • 컨테이너의 시간대를 맞춰 둔다(TZ: Asia/Seoul). 안 그러면 8시가 UTC 8시가 된다.
  • 하루 한 번 보장은 상태 파일로. 8시 직후에 컨테이너가 재시작하면 같은 알림이 다시 나갈 수 있다. 마지막으로 보낸 날짜를 볼륨의 파일에 적어 두고, 같은 날이면 건너뛴다.
  • 즉시 한 번 실행하는 명령행 옵션을 둔다. 내일 아침까지 기다리지 않고 형식을 확인할 수 있다.

다음 편은 알림 봇의 핵심인 "새 것만 추리기"다. 시리즈 전체 목록은 여기.

내 일을 대신 지켜보는 알림 봇 만들기 (2) 새 것만 알리기와 오픈마켓 API 함정

알림봇-따라하기 시리즈 2편. 주기적으로 조회해서 "지난번 이후 새로 생긴 것"만 알리는 부분을 만든다. 예시는 온라인 판매 채널 세 곳의 새 주문·미답변 문의 알림이다. 모두 2026년 가을에 직접 붙이면서 확인한 내용이다.

상태 파일: 어디까지 봤는가

봇마다 작은 JSON 파일 하나를 볼륨에 둔다. 담는 것은 두 가지다.

{
  "lastFrom": "2026-11-01T09:40:00+09:00",
  "seen": { "주문번호:옵션번호": "2026-11-01T09:42:10+09:00" }
}
  • 조회 시작 시각(lastFrom): 다음 조회는 여기서부터 한다.
  • 이미 알린 것의 키(seen): 조회 구간을 일부러 겹치게 잡아도 중복 알림이 나가지 않게 한다. 오래된 키는 주기적으로 지운다.

규칙은 세 가지다.

  1. 조회에 실패하면 lastFrom 을 전진시키지 않는다. 실패한 구간을 건너뛰면 그 사이의 주문은 영영 알림이 안 온다. 다음 주기에 같은 구간을 다시 본다.
  2. 알림 전송에 성공한 뒤에 seen 에 넣는다. 반대로 하면 전송이 실패했을 때 그 건이 사라진다.
  3. 중복 키는 "한 건"의 단위와 맞춘다. 주문 하나에 상품이 여럿이면 주문번호만으로는 부족하다. 주문번호:상품옵션번호 처럼 합친다.

상태가 바뀌는 속도가 조회 주기보다 빠를 때

"결제 완료" 상태의 주문만 조회해 새 주문을 알렸는데, 가끔 알림 없이 지나가는 주문이 있었다. 판매자가 주문을 빨리 확인하면 20분 주기의 조회가 오기 전에 상태가 "상품 준비 중"으로 넘어가 버린 것이다. 다음 상태도 함께 조회하고 중복 키로 한 번만 알리게 해서 해결했다. 상태로 걸러 조회하는 API 는 그 상태에 머무는 시간이 내 조회 주기보다 짧을 수 있다는 점을 따져 봐야 한다.

목록은 한 번에 다 오지 않는다

다른 봇에서 겪은 일이다. 체결 내역 조회가 20건씩 끊겨 오는 API 였는데 첫 장만 읽고 있었다. 건수가 적을 때는 문제가 없다가 바쁜 날 21번째부터의 항목을 통째로 놓쳤다. 목록 API 를 붙일 때는 "다음 장이 있다"는 표시(연속 조회 헤더, 페이지 번호, 커서)를 먼저 찾아 끝까지 읽는다.

판매 채널별로 겪은 것

네이버 커머스API

  • 토큰을 받을 때 클라이언트 시크릿을 그대로 보내지 않고 bcrypt 로 서명한 값을 보낸다. Node 내장 모듈에는 bcrypt 가 없어서 이 봇만 예외로 외부 라이브러리 하나를 썼다.
  • cron 에서 컨테이너 안의 스크립트를 실행할 때, 스크립트를 이미지 밖(마운트한 폴더)에 두면 라이브러리 경로를 못 찾는다. NODE_PATH 로 이미지 안의 node_modules 를 가리켜야 했다.

11번가 셀러 OpenAPI

  • 응답이 EUC-KR 로 인코딩된 XML 이다. UTF-8 로 읽으면 한글이 전부 깨진다. 바이트로 받아 new TextDecoder('euc-kr') 로 디코드한다.
  • 주문 조회의 시각은 분 단위 12자리, 문의 조회는 일 단위 8자리로 형식이 다르고, 한 번에 볼 수 있는 기간도 각각 7일, 30일로 다르다.
  • 문의 본문이 HTML 엔티티로 두 번 인코딩돼 온다. 한 번만 풀면 < 같은 글자가 남는다.
  • 문의 목록에는 고객 문의와 시스템 안내가 섞여 있다. 처리 상태 코드로 "아직 답하지 않은 것"만 걸러서 알린다.
  • 처음 실데이터가 들어온 한 건은 원본 XML 을 로그에 통째로 남기게 했다. 문서의 필드 이름이 실제와 같은지 확인하는 가장 빠른 방법이다.

쿠팡 오픈API

  • 요청마다 HMAC-SHA256 서명을 만든다. 서명할 문자열은 시각 + 메서드 + 경로 + 쿼리 이고, 쿼리는 물음표 없이 인코딩된 그대로 붙인다. 시각은 UTC 로 yyMMddTHHmmssZ 형식이다.
    const crypto = require('node:crypto');
    function sign(method, path, query, accessKey, secretKey) {
      const d = new Date().toISOString().replace(/[-:]/g, '').slice(2, 15) + 'Z';   // yyMMddTHHmmssZ
      const sig = crypto.createHmac('sha256', secretKey).update(d + method + path + query).digest('hex');
      return `CEA algorithm=HmacSHA256, access-key=${accessKey}, signed-date=${d}, signature=${sig}`;
    }
  • 호출하는 서버의 공인 IP 를 판매자 센터에 등록해야 한다. 등록하지 않으면 키가 맞아도 403 이 온다. 반대로 가짜 키로 부르면 401 이 오는데, 이것으로 경로와 헤더 형식이 맞는지는 키를 받기 전에도 확인할 수 있다.
  • 분 단위 주문 조회는 범위가 24시간 미만이어야 한다. 넘으면 400.
  • 날짜 파라미터는 시간대까지 써야 한다. 날짜만 보내면 형식 오류가 나고, +09:00 의 + 는 URL 에서 %2B 로 인코딩한다(안 하면 공백으로 해석된다).
  • 금액 필드가 숫자가 아니라 {통화, 단위, 나노} 객체다. 예전 예제를 보고 숫자로 다루면 알림에 [object Object] 가 찍힌다.
  • 상품 문의 응답에는 상품 이름이 없고 상품 번호만 있다. 주문을 조회할 때 본 "번호 → 이름"을 상태 파일에 캐시해 두었다가 문의 알림에 쓴다.

시험 방법

  • --dry-run: 조회는 하되 채팅에 보내지 않고 상태도 저장하지 않는다. 몇 번을 돌려도 같은 결과가 나오므로 형식을 다듬기 좋다.
  • 상태 파일 경로를 환경 변수로 바꿀 수 있게 해 두면, 운영 상태를 건드리지 않고 "처음 실행" 상황을 재현할 수 있다.
  • 첫 실행에서는 과거 것을 전부 알리지 않도록, 상태 파일이 없으면 "지금"부터 시작한다.

다음 편은 이 알림을 메신저로도 받는 방법이다. 시리즈 전체 목록은 여기.

내 일을 대신 지켜보는 알림 봇 만들기 (3) 카카오톡 "나에게 보내기"로 사본 받기

알림봇-따라하기 시리즈 마지막 편. 채팅방에 올라가는 알림 중 놓치면 안 되는 몇 가지를 카카오톡으로도 받는다. 카카오 로그인의 "나에게 보내기" 기능을 쓰면 별도 채널이나 비용 없이 내 카카오톡으로 메시지를 보낼 수 있다.

왜 중계 서버를 따로 두나

알림을 보내는 봇은 여럿인데, 카카오 토큰은 하나다. 봇마다 토큰을 들고 각자 갱신하면 서로의 토큰을 무효로 만들 수 있다. 그래서 토큰을 혼자 소유하는 작은 서버를 하나 두고, 봇들은 그 서버에 "이 문장을 보내 줘"라고만 요청한다.

봇 A ─┐
봇 B ─┼─ POST /send {text, link} ──▶ 중계 서버 ──▶ 카카오 API ──▶ 내 카카오톡
봇 C ─┘        (내부 토큰 헤더)        (토큰 파일 단독 소유)

카카오 쪽 준비

  1. 개발자 콘솔에서 애플리케이션을 만들고 REST API 키를 확인한다.
  2. 카카오 로그인을 켜고 Redirect URI 를 등록한다. 서버에 공개 주소가 없어도 된다. http://localhost/callback 처럼 열리지 않는 주소를 등록해 두고, 동의 뒤 브라우저 주소창에 남은 주소에서 인가 코드를 복사해 쓰면 된다.
  3. 동의 항목에서 "카카오톡 메시지 전송"을 켠다.
  4. 메시지의 버튼이 열 사이트 도메인을 등록한다. 등록하지 않은 도메인의 링크는 버튼을 눌러도 열리지 않는다.

겪은 함정 두 가지.

  • 지금 콘솔에서는 REST API 키를 만들면 클라이언트 시크릿이 기본으로 켜져 있다. 예전 예제대로 키만 보내면 토큰 발급이 실패한다. 시크릿을 함께 보내거나 콘솔에서 끈다.
  • 콘솔 메뉴 구성이 바뀌어서 검색으로 나오는 안내의 메뉴 위치가 맞지 않았다. Redirect URI 와 시크릿은 키 설정 쪽에, 링크 도메인은 제품 링크 관리 쪽에 있었다.

토큰 관리

인가 코드를 토큰으로 바꾸면 액세스 토큰(수 시간)과 리프레시 토큰(약 두 달)이 온다. 중계 서버는 이것을 볼륨의 파일 하나에 저장한다.

async function accessToken() {
  if (Date.now() > tok.access_expires - 10 * 60e3) await refresh();   // 만료 10분 전이면 갱신
  return tok.access_token;
}

// 알림이 없는 날이 길어도 리프레시 토큰이 죽지 않게 하루 한 번 갱신
setInterval(() => refresh().catch(logError), 24 * 3600e3).unref();
  • 리프레시 토큰은 갱신할 때마다 새로 오지 않는다. 남은 기간이 한 달 아래로 내려갔을 때만 새 값이 온다. 응답에 없으면 기존 값을 유지해야 한다. 응답의 값을 무조건 덮어쓰면 빈 값이 저장된다.
  • 그래서 주기적으로 갱신하는 것이 곧 연장이다. 알림이 드문 봇이라도 하루 한 번 갱신해 두면 두 달 뒤에 조용히 끊기는 일이 없다.
  • 파일은 임시 파일에 쓴 뒤 이름을 바꿔 교체한다. 쓰는 도중 프로세스가 죽어도 반쪽짜리 파일이 남지 않는다.
  • 전송에서 401 이 오면 한 번만 갱신 후 재시도한다. 그래도 실패하면 상태 확인 주소에 오류를 남기고, 사람이 동의 절차를 다시 밟는다.

200자 제한과 묶음 전송

텍스트 템플릿의 본문은 200자까지다. 채팅방용으로 만든 알림을 그대로 보낼 수 없어서 중계 서버가 손본다.

// 채팅용 마크다운을 평문으로
const plain = (s) => String(s || '')
  .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')   // [글](링크) → 글
  .replace(/\*\*|__|`/g, '')                  // 굵게·코드 기호 제거
  .replace(/^#+\s*/gm, '')
  .trim();
  • 200자를 넘으면 자르고 말줄임표를 붙인다. 전문은 채팅방에 있으니 사본은 "무슨 일이 생겼는지"만 전하면 된다.
  • 3초 동안 들어온 요청을 모아 한 통으로 보낸다. 같은 일이 연달아 여러 번 일어나면 알림이 몇 초 사이에 줄줄이 온다. 같은 링크의 메시지는 200자가 찰 때까지 줄바꿈으로 이어 붙인다.
  • 요청에는 큐에 넣자마자 202 로 바로 응답한다. 보내는 쪽 봇이 카카오 API 의 응답을 기다리며 멈추지 않는다.

내부에서만 부를 수 있게

  • 포트는 루프백과 컨테이너 브리지 주소에만 연다. 다른 compose 프로젝트의 컨테이너는 닿을 수 있지만 LAN 과 인터넷에서는 보이지 않는다.
  • 요청에는 공유 토큰 헤더를 요구한다. 비교는 해시한 뒤 crypto.timingSafeEqual 로 한다.
  • 토큰은 서버에서 무작위로 만들어 양쪽 설정 파일에 넣는다. 값을 화면에 찍지 않고도 양쪽이 같은지 확인하려면 각각의 해시를 비교하면 된다.
  • 상태 확인 주소는 동의 여부, 만료 시각, 보낸 수와 실패 수, 마지막 오류만 보여 준다. 토큰 값은 내보내지 않는다.

보내는 쪽: 실패해도 본업은 계속

// 채팅 게시와 메신저 사본을 묶는다. 사본이 실패해도 게시 결과는 그대로 돌려준다
function fanout(chat, relay) {
  return {
    async post(message, link) {
      const ok = await chat.post(message);
      relay.send(message, link).catch((e) => console.error('사본 실패', e.message));
      return ok;
    },
  };
}

사본은 덤이다. 중계 서버가 내려가 있어도 채팅 알림과 봇의 상태 진행에는 영향이 없어야 한다. 반대로 채팅 게시가 실패했다면 사본도 보내지 않는다. 사본만 오고 원문이 없으면 나중에 찾아볼 곳이 없다.

시리즈를 마치며

알림 봇은 코드가 짧아서 금방 만들지만, 믿고 쓰려면 세 가지가 맞아야 했다. 같은 알림이 두 번 오지 않을 것, 빠뜨리지 않을 것, 그리고 알림 경로 하나가 죽어도 나머지가 계속 돌 것. 이 시리즈의 대부분은 결국 그 세 가지 이야기였다.

시리즈 전체 목록은 여기, 봇을 올릴 서버는 홈서버-따라하기에 있다.

나만의 아침 대시보드 만들기 (0) 전체 구성

아침에 확인하는 것들 — 날씨, 일정, 뉴스, 교통, 시세 — 을 한 화면에 모으고, 버튼 하나로 읽어 주기까지 하는 개인 대시보드를 만들어 쓰고 있다. 태블릿을 거치대에 세워 두고 출근 준비를 하면서 본다. 어떻게 만들었는지 묻는 사람이 있어서 몇 편으로 나눠 적는다. 각 편은 대시보드-따라하기 라벨에 모여 있다.

만들면서 정한 원칙

  • 외부 패키지 없이. Node 24 의 내장 기능(http, fetch, node:sqlite, --env-file)만 쓴다. npm install 이 없으니 빌드가 빠르고, 몇 달 뒤에 열어도 깨져 있지 않다.
  • 화면은 HTML 파일 하나. 프레임워크도 번들러도 없다. 서버가 주는 JSON 을 60초마다 받아서 다시 그린다.
  • 카드 하나 = 수집기 파일 하나. 새 카드를 붙일 때 건드리는 곳이 정해져 있어야 계속 늘릴 수 있다.
  • 수집이 실패해도 화면은 멀쩡하게. 외부 API 는 반드시 죽는다. 실패하면 직전 값을 그대로 보여 주고 조용히 다시 시도한다.

전체 구성

외부 API / RSS / 캘린더
        │  (수집기마다 정해진 주기)
        ▼
  collectors/*.js ──▶ 스케줄러(메모리에 최신값 보관) ──▶ GET /api/state
                              │                              │
                         LLM 요약·번역                  index.html (60초 폴링)
                              │
                       브리핑 원고 ──▶ TTS ──▶ 음성 재생

편별 내용

  1. 뼈대 만들기 — 의존성 없는 서버, 수집기 규약, 실패에 강한 스케줄러, 태블릿 화면 요령
  2. 데이터 모으기 — 키 없이 쓸 수 있는 소스들과 공공 API 에서 밟은 함정
  3. LLM 붙이기 — 번역·요약·브리핑 원고, 그리고 비용을 수십 분의 일로 줄인 방법
  4. 읽어 주는 대시보드 — 음성 합성, 기다리지 않고 바로 재생하기, 모바일 브라우저의 제약

배포

컨테이너 하나로 돌린다. 서버에 올리고 주소를 붙이고 로그인 뒤로 숨기는 과정은 홈서버-따라하기 시리즈에 따로 적었다. 개인 일정과 관심사가 그대로 드러나는 화면이므로 공개 주소에 그냥 올려 두지 말고 반드시 인증을 앞에 세운다.

미리 말해 두는 것

이런 프로젝트는 "완성"이 없다. 처음에는 카드 여덟 장이었는데 쓰다 보니 스무 장을 넘겼다. 그래서 이 시리즈도 특정 카드의 코드보다, 카드를 계속 붙여도 무너지지 않는 구조와 그 과정에서 겪은 문제에 무게를 둔다.

나만의 아침 대시보드 만들기 (1) 의존성 없는 뼈대와 수집 스케줄러

대시보드-따라하기 시리즈 1편. 카드를 스무 장 넘게 붙여도 버티는 뼈대를 만든다. 핵심은 "수집기 규약"과 "실패에 강한 스케줄러" 두 가지다.

폴더 구조

server.js            HTTP 서버, /api/state, 정적 파일
lib/scheduler.js     수집기를 주기적으로 실행
lib/http.js          타임아웃·오류 처리를 넣은 fetch 래퍼
collectors/*.js      카드 하나당 파일 하나
config/*.json        종목·검색어 같은 설정
public/index.html    화면 전부

수집기 규약

수집기는 이름, 주기, 그리고 데이터를 돌려주는 함수만 내보낸다.

// collectors/weather.js
module.exports = {
  name: 'weather',
  title: '날씨',
  intervalMs: 10 * 60e3,
  timeoutMs: 20e3,
  enabled: (env) => !!env.WEATHER_LAT,      // 설정이 없으면 카드를 끈다
  async fetch(ctx) {
    const r = await ctx.http.json('https://api.example.com/forecast?...');
    return { now: r.current, hourly: r.hourly.slice(0, 12) };
  },
};

server.js 는 수집기를 폴더 스캔이 아니라 명시 목록으로 불러온다. 순서가 곧 화면 순서이고, 지운 파일이 실수로 다시 실행되는 일도 없다.

스케줄러

setInterval 대신 수집기마다 setTimeout 을 이어 붙인다. 한 번 끝난 뒤에 다음 실행을 잡으므로 느린 API 때문에 실행이 겹치지 않는다.

const MAX_BACKOFF = 30 * 60e3;

async function run(c) {
  const s = state[c.name];
  if (running[c.name]) return;
  running[c.name] = true;
  let timer;
  try {
    const data = await Promise.race([
      c.fetch(ctx),
      new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('timeout')), c.timeoutMs || 60e3); }),
    ]);
    Object.assign(s, { data, updatedAt: Date.now(), error: null, failures: 0 });
  } catch (e) {
    s.error = e.message; s.failures++;      // data 는 건드리지 않는다 → 화면엔 직전 값
  } finally {
    clearTimeout(timer);
    running[c.name] = false;
    const delay = s.failures ? Math.min(c.intervalMs * 2 ** s.failures, MAX_BACKOFF) : c.intervalMs;
    setTimeout(() => run(c), delay);
  }
}
  • 실패해도 data 를 지우지 않는다. 화면에는 직전 값과 "몇 분 전 갱신"이 보인다. 카드가 텅 비는 것보다 조금 오래된 값이 낫다.
  • 지수 백오프. 실패가 이어지면 간격을 두 배씩 늘리되 30분을 넘기지 않는다. 죽은 API 를 두들겨서 차단당하는 일을 막는다.
  • 첫 실행은 0~5초 사이 무작위 지연. 기동 직후 스무 개가 한꺼번에 나가면 같은 호스트의 호출 제한에 걸린다.

겪은 함정: "지금 실행"이 건너뛰어진다

화면에서 값을 저장한 직후 해당 수집기를 즉시 다시 돌리는 runNow() 를 만들었는데, 가끔 저장 전 값이 그대로 보였다. 마침 그 수집기가 실행 중이면 running 검사에 걸려 그냥 돌아와 버렸기 때문이다. 진행 중인 실행은 저장 전 데이터를 읽고 있었다.

async runNow(name) {
  if (inflight[name]) await inflight[name];   // 진행 중인 실행이 끝나길 기다리고
  await start(byName[name]);                  // 한 번 더 돌린다
  return state[name];
}

"중복 실행 방지"와 "최신 값 보장"은 다른 요구다. 건너뛰기만 하면 앞의 것만 만족한다.

화면: 태블릿에서 배운 것

  • 카드 안쪽 스크롤을 없애고 페이지 스크롤 하나만 남겼다. 터치로 넘기다 카드 안에 갇히는 일이 사라진다. 긴 목록은 "더보기"로 펼친다.
  • 안쪽 스크롤을 없애면 줄바꿈 안 되는 긴 문구가 카드 밖으로 삐져나와 페이지가 가로로 넓어진다. .card { overflow: hidden } 과 html, body { overflow-x: clip } 로 막았다.
  • 60초마다 다시 그릴 때 펼침 상태, 선택한 탭이 초기화되지 않게 상태를 따로 기억한다.
  • 지도 카드는 터치 기기에서 전체화면일 때만 드래그를 받는다. 그러지 않으면 페이지를 내리려다 지도만 움직인다.
  • 새로고침하면 브라우저가 이전 스크롤 위치로 돌아가려 한다. 대시보드는 항상 맨 위에서 시작하는 편이 낫다: history.scrollRestoration = 'manual'.

로컬에서 돌리기

node --env-file=.env server.js

수집기 하나만 따로 실행해 결과를 찍어 보는 스크립트를 만들어 두면 새 카드를 붙일 때 서버를 띄우지 않아도 된다.

다음 편은 카드에 넣을 데이터를 어디서 구하는지다. 시리즈 전체 목록은 여기.

나만의 아침 대시보드 만들기 (2) 키 없이 쓰는 데이터 소스와 공공 API 함정

대시보드-따라하기 시리즈 2편. 카드에 넣을 데이터를 어디서 가져왔는지, 그리고 문서와 실제가 달랐던 곳들을 적는다. 모두 2026년 가을 기준으로 직접 확인한 내용이라 이후에 바뀌었을 수 있다.

가입 없이 시작할 수 있는 것

  • 날씨: Open-Meteo. 키가 없고 시간별 예보, 대기질까지 준다. 처음엔 이것 하나로 충분하다.
  • 뉴스: Google News RSS. 주제 섹션 피드와 검색어 피드가 있어서 설정 파일의 검색어만 바꾸면 탭이 늘어난다.
  • 일정: 캘린더의 비공개 iCal(ICS) 주소. OAuth 없이 URL 하나로 읽을 수 있다. 다만 반복 일정(RRULE)은 직접 펼쳐야 한다. 공휴일도 공개 ICS 로 받아서 "오늘이 쉬는 날인가"를 판단하는 데 쓴다.
  • 교통 소통 지도: 국가교통정보센터의 실시간 소통 타일이 지도 타일(WMTS) 형태로 제공된다. 지도 라이브러리의 베이스맵 위에 레이어로 겹치면 된다. 해당 기관 페이지는 iframe 으로 넣을 수 없게 막혀 있어서 타일을 직접 쓰는 방식이어야 했다.
  • 개발 뉴스: Hacker News 는 검색 API 로 프런트 페이지를, 국내 커뮤니티는 Atom 피드를 읽는다.
  • 역사 속 오늘: 위키백과의 날짜 문서를 API 로 받아 섹션을 파싱한다.

공공 API 에서 밟은 것들

  • 공공데이터포털 키 하나로 다 되지 않는다. 인증키는 계정에 하나지만 API 마다 활용신청을 해야 한다. 신청하지 않은 API 를 부르면 "등록되지 않은 서비스" 계열 오류(코드 30)가 오는데, 키가 틀렸다는 뜻으로 읽기 쉽다.
  • 버전이 조용히 바뀐다. 기상청 생활기상지수는 경로에 버전이 들어 있는데, 검색해서 나오는 예제의 구버전 경로는 "서비스 없음"(코드 12)을 돌려줬다. 포털의 현재 명세에서 경로를 다시 확인해야 한다.
  • 연산 이름 철자. 에어코리아의 측정소별 실시간 조회는 연산 이름 중간이 Accto 다. 한 글자 다르게 쓴 예제가 돌아다니는데 그대로 쓰면 코드 12 가 온다. 측정소 이름도 공식 명칭 그대로여야 하고, 줄여 쓰면 오류 없이 0건이 온다.
  • 응답이 문서와 다르다. 고속도로 돌발 정보 API 는 실제 응답의 키 이름과 날짜 형식이 문서와 달랐고, 몇 해 전 홍보성 항목이 계속 내려왔다. 최근 24시간으로 걸러서 쓴다.
  • 키가 없으면 5행. 교육 정보 공개 API 는 키 없이 부르면 5행만 주는데, 페이지 번호를 바꿔도 같은 5행이 온다. 기간을 반으로 쪼개 가며 조회하는 식으로 우회했다. 키를 받는 편이 속 편하다.
  • 좌표계. 주유소 가격 API 는 좌표를 위경도가 아닌 국내 좌표계(KATEC)로 준다. 지도에 찍으려면 변환이 필요하다. 변환식을 직접 구현했고 실측 오차는 수십 미터 안이었다.

비공식·무료 소스에서 밟은 것들

  • 전일 대비 계산. 널리 쓰이는 주가 차트 API 의 메타 정보에 "이전 종가"처럼 보이는 필드가 있는데, 이것은 요청한 차트 범위가 시작되기 전의 종가다. 전일 대비 등락률에 쓰면 틀린다. 일봉의 마지막 두 봉을 비교해야 한다.
  • 호출 제한은 생각보다 빡빡하다. 어떤 엔드포인트는 연속 7번째 호출에서 429 를 줬고(간격을 2.5초로 벌려 해결), 어떤 경제 일정 JSON 은 5분에 두세 번만 불러도 막혔다.
  • 개발 PC 와 서버가 같은 공인 IP 를 쓰면 로컬에서 시험한 호출이 서버 몫의 한도를 깎아 먹는다. 배포 직후 서버 쪽 카드가 비어서 알았다. 이런 소스는 응답을 디스크에 캐시하고, 429 를 받으면 한 시간 쉬게 했다.
  • 주소가 바뀐다. 음악 차트 RSS 는 호스트가 옮겨 가며 리다이렉트가 생겼고, 다른 차트 사이트는 어느 날 유료벽 뒤로 들어가 HTML 에서 순위가 사라졌다. 비공식 소스에 기대는 카드는 "없으면 조용히 빠지는" 구조여야 한다.
  • 검색 API 의 언어 옵션은 결과를 걸러 주지 않는다. 한국어 영상만 원하면 제목에 한글이 있는지 직접 봐야 했다. 제목은 HTML 엔티티로 이스케이프돼 오므로 디코드도 필요하다.

컨테이너에서만 타임아웃이 날 때

로컬에서는 잘 되던 호출이 컨테이너에서만 타임아웃이 났다. 대상 호스트가 IPv6 주소를 함께 내주는데 컨테이너 네트워크에는 IPv6 경로가 없어서, Node 가 IPv6 를 먼저 시도하다 멈춘 것이다. Dockerfile 에 한 줄을 넣어 해결했다.

ENV NODE_OPTIONS="--dns-result-order=ipv4first"

로그에 키를 남기지 않기

공공 API 는 인증키를 URL 쿼리에 넣는다. 오류가 날 때 URL 을 통째로 로그에 찍으면 키가 로그에 남는다. HTTP 래퍼에서 오류 메시지를 만들 때 쿼리 문자열을 떼어 낸다.

const safe = (url) => String(url).split('?')[0];
throw new Error(`HTTP ${res.status} ${safe(url)}`);

다음 편에서는 이렇게 모은 데이터를 LLM 으로 번역·요약하고, 그 비용을 관리하는 이야기를 한다. 시리즈 전체 목록은 여기.

나만의 아침 대시보드 만들기 (3) LLM 붙이기와 비용 줄이기

대시보드-따라하기 시리즈 3편. 모은 데이터에 LLM 을 붙인다. 해외 기사 제목 번역, 급등락 종목의 이유 한 줄, 오늘의 메뉴, 그리고 전체를 엮은 브리핑 원고까지. 기능을 붙이는 것보다 비용과 품질을 다루는 쪽이 더 어려웠다.

SDK 없이 호출하기

의존성을 두지 않는 원칙대로 메시지 API 를 fetch 로 직접 부른다. 헬퍼는 세 가지만 만들었다.

  • json({system, user, schema}) — 구조화된 JSON 을 받는다. 카드에 넣을 데이터는 전부 이것.
  • text() — 질문 답변처럼 자유 형식.
  • stream() — 긴 원고를 스트리밍으로 받는다(다음 편의 음성에서 쓴다).

헬퍼 안에서 호출마다 입력·출력 토큰과 추정 비용을 누적해 상태 확인 주소로 내보낸다. 이 숫자가 없었다면 아래의 문제를 한참 늦게 알았을 것이다.

비용이 새는 곳은 "자주 도는 것"이다

이틀쯤 돌린 뒤 누적 비용을 보니 생각보다 훨씬 컸다. 범인은 화려한 브리핑이 아니라 5분마다 도는 사소한 호출이었다. 급등락 종목의 이유를 한 줄로 요약하는 작업을 가장 큰 모델로 돌리고 있었고, 한 번은 몇 센트지만 하루 수백 번이면 전체의 대부분이 된다.

모델을 두 단으로 나눴다.

  • 가벼운 모델: 번역, 한 줄 요약, 헤드라인처럼 자주 돌고 형식이 단순한 것. 내 경우 단가가 약 40배 쌌다.
  • 큰 모델: 하루 한두 번 도는 긴 글(브리핑 본문, 오늘의 메뉴 등).

그리고 호출 자체를 줄였다.

  • 새로운 것만 묶어서 한 번에. 기사 제목 번역은 이미 번역한 제목을 캐시에 두고, 새 제목만 모아 한 번 호출한다.
  • 하루 한 번이면 되는 것은 하루 한 번. 결과를 날짜 키로 디스크에 저장하고, 있으면 다시 만들지 않는다.
  • 유효 시간을 늘린다. "이유 한 줄"은 30분마다 바뀔 필요가 없었다.

겪은 함정: 가벼운 모델이 답 대신 생각을 한다

가벼운 모델로 옮기자 50자짜리 답이 중간에 잘려 왔다. 확인해 보니 그 모델은 사고(thinking)가 기본으로 켜져 있어서, 짧은 답 하나에 천 토큰 넘게 생각을 쓰고는 출력 한도에 걸려 끊긴 것이었다. 사고를 끄니 1초 안에 온전한 답이 왔다.

{
  "model": "<가벼운 모델>",
  "max_tokens": 480,
  "thinking": { "type": "disabled" },
  "messages": [ ... ]
}

대신 부작용이 있었다. 사고를 끈 모델은 "300자 안팎으로" 같은 길이 지시를 잘 지키지 않았다. 출력 한도를 빡빡하게 주고, 잘린 경우 마지막 문장 끝에서 잘라 쓰는 후처리를 넣었다. 또 프롬프트 뒤쪽의 지시를 흘리는 경향이 있어서, 절대 어기면 안 되는 지시는 맨 앞에 둔다.

매일 만드는 글이 매일 비슷해진다

"오늘의 메뉴"를 며칠 돌리면 비슷한 음식만 나온다. 두 가지를 섞어 해결했다.

  • 최근 2주 동안 낸 것의 목록을 프롬프트에 넣고 피하라고 한다.
  • 날짜로 계산한 순환 변수(장르·주재료·조리법)를 준다. 주기를 7, 9, 8 처럼 서로 다르게 잡으면 조합이 오래 겹치지 않는다.

퀴즈처럼 중복이 더 거슬리는 것은 석 달 치 기록을 쌓아 두고 피하게 했다.

브리핑 원고: 규칙만으로는 안 지켜지는 것들

  • 말투. 아무 지시 없이 쓰게 하면 "다음은 날씨입니다" 식의 뉴스 진행 말투가 된다. "섹션 이름을 외치지 말 것"을 명시해야 비서가 말해 주는 듯한 문장이 나온다.
  • 쉬는 날. 금요일 저녁 브리핑이 "내일 출근길"을 이야기했다. 공휴일 캘린더로 다음 날의 종류(평일·주말·공휴일)를 계산해 넘겨도 가끔 새어 나와서, 쉬는 날이면 그 사실을 프롬프트 맨 앞에 쓰고, 생성된 원고에 "내일 … 출근"이 있으면 그 문장만 다시 쓰게 하는 검사를 붙였다. 프롬프트로 부탁하는 것과 결과를 검사하는 것은 다른 층이다.
  • 이름. 일정에 사람 이름이 있으면 원고가 그대로 읽는다. 누구인지는 판별용으로만 주고, 원고에는 관계("가족 일정")로만 쓰게 한다.
  • 실패했을 때. LLM 호출이 실패하면 템플릿으로 만든 단순한 원고로 대신한다. 아침에 버튼을 눌렀는데 아무 소리도 안 나는 것이 가장 나쁘다.

캐시와 개발 서버

프롬프트를 고친 뒤 "강제 새로 생성"을 눌렀는데 옛 말투가 그대로 나온 적이 있다. 서버 프로세스가 고치기 전 모듈을 메모리에 들고 있었기 때문이다. 프롬프트가 코드에 들어 있으면 고칠 때마다 서버를 다시 띄워야 한다.

다음 편은 이 원고를 소리로 바꾸는 이야기다. 시리즈 전체 목록은 여기.

나만의 아침 대시보드 만들기 (4) 읽어 주는 대시보드 — TTS 와 모바일 재생

대시보드-따라하기 시리즈 마지막 편. 버튼을 누르면 오늘의 브리핑을 읽어 주게 만든다. 화면을 볼 수 없는 시간에 쓰려고 붙였는데, 시리즈 전체에서 가장 손이 많이 간 부분이다.

1단계: 브라우저 음성으로 시작

처음에는 브라우저의 Web Speech API 로 읽었다. 무료이고 코드 몇 줄이면 된다. 다만 기기마다 목소리가 다르고 긴 글에서 어색하다. 지금도 음성 합성 서버가 막혔을 때의 대체 수단으로 남겨 두었다.

2단계: 서버에서 합성

LLM 계열 음성 합성 API 로 옮겼다. 텍스트를 보내면 PCM 오디오가 온다. 붙이면서 알게 된 것들:

  • 입력 길이 제한이 있어서 긴 원고는 2,500자 안팎으로 나눠 합성한 뒤 PCM 을 이어 붙이고 WAV 헤더를 씌운다.
  • 말투 지시문을 본문에 넣지 않는다. "다음 글을 차분하게 읽어 줘" 같은 문장을 앞에 붙였더니, 가끔 그 지시문까지 소리 내어 읽었다. 내가 쓴 모델은 시스템 지시를 따로 받지 않아서, 결국 지시문을 전부 뺐다.
  • 가끔 비정상적으로 긴 음성이 나온다. 글자 수로 예상 길이를 계산해 두고(한국어는 글자당 0.2초쯤), 그보다 훨씬 길면 다시 합성해서 가장 짧은 것을 쓴다.
  • 숫자 읽기. "13도"를 "열세 도", "40분"을 "마흔 분"으로 읽을 때가 있다. 합성 직전에 단위가 붙은 숫자를 한글로 풀어 써서("십삼 도", "사십 분") 넘긴다.
  • 요금제를 먼저 본다. 후보였던 다른 음성 서비스는 호출이 없어도 월 기본요금이 꽤 컸다. 콘솔에서 확인하고 접었다.

3단계: 기다리지 않게 하기

원고를 다 쓰고 통째로 합성하니 버튼을 누르고 1분 넘게 조용했다. 재서 보니 원고 작성에 40~80초, 합성에 30초 남짓이었다. 미리 만들어 두는 방법도 있지만, 아무도 듣지 않는 날에도 비용이 나간다. 대신 조각내어 흘려보내기로 했다.

  1. 오프닝은 가벼운 모델로 2~4초 만에 만든다. 인사와 가장 중요한 한두 가지.
  2. 본문은 큰 모델에서 스트리밍으로 받으면서, 문장이 240자쯤 쌓일 때마다 한 파트로 잘라 합성 큐에 넣는다. 합성은 동시에 두 개까지.
  3. 클라이언트는 첫 파트가 준비되는 대로 재생을 시작하고, 재생 중에 다음 파트가 준비됐는지 확인해 큐 뒤에 붙인다.

이렇게 해서 첫 소리가 나기까지의 시간이 몇 초로 줄었다. 파트 끝에는 짧은 무음을 붙여 이어지는 느낌을 맞췄다.

호출 한도: 분당, 그리고 하루

  • 분당 한도. 서버가 재시작한 직후 예약된 합성과 사용자의 요청이 겹치자 429 가 났다. 전역 동시 실행을 2개로 묶고, 429 면 8·16·32초 뒤에 다시 시도한다.
  • 하루 한도. 어느 날 오후부터 음성이 전혀 나오지 않았다. 그날 시험을 많이 돌려 모델별 하루 요청 한도를 다 쓴 것이다. 문제는 그다음이었다. 429 응답이 "약 23시간 뒤에 다시 시도"라고 알려 줬는데, 내 재시도 코드가 그 시간을 그대로 기다렸다. 합성 슬롯 두 개가 모두 하루짜리 대기에 묶여서 새 요청은 영영 차례가 오지 않았다.

고친 방식: 서버가 알려 준 대기 시간이 2분을 넘으면 기다리지 않고 그 모델을 "소진"으로 표시한 뒤 한도가 따로인 예비 모델로 넘긴다. 예비도 막히면 즉시 실패를 돌려주고, 클라이언트는 브라우저 음성으로 읽는다. 소진 상태는 상태 확인 주소에 노출한다. 재시도는 "언제까지 기다릴 가치가 있는가"의 상한이 있어야 한다.

모바일 브라우저의 제약

  • Range 요청. iOS Safari 는 오디오를 부분 요청(Range)으로 받는다. 서버가 206 응답을 지원하지 않으면 재생이 안 되거나 탐색이 안 된다.
  • 자동 재생. 사용자의 터치로 시작된 재생이 끊기면 다음 재생을 코드로 시작할 수 없다. 퀴즈의 "생각할 시간"이나 다음 파트를 기다리는 동안에는 무음 오디오를 큐에 끼워 재생이 이어지게 했다.
  • 재생 취소 처리. 재생을 새로 시작할 때마다 일련번호를 올리고, 진행 중이던 비동기 작업은 자기 번호가 최신이 아니면 물러나게 했다. 그런데 준비 상태를 확인하는 타이머가 번호를 올리기 전에 읽어 두는 바람에 첫 확인에서 스스로 꺼졌고, 파트 사이마다 2초씩 공백이 생겼다. 기준 번호는 재생을 시작한 뒤의 값이어야 한다.
  • 차량 블루투스. 안드로이드 Chrome 에서 음성 질문을 받으려고 마이크를 여는 순간, 운영체제가 블루투스를 통화 모드로 바꿔 차량 화면에 "통화 중"이 떴다. 웹에서는 확실히 피할 방법을 찾지 못했다. 그래서 질문은 글 입력을 기본으로 하고, 음성 입력은 필요할 때만 켜게 바꿨다.

시험할 때 주의

로컬에서 서버를 띄우면 그날 치 음성이 없다는 이유로 합성을 처음부터 다시 한다. 하루 한도를 쓰는 작업이므로, 화면만 고칠 때는 음성 수집기를 꺼 두는 환경 변수를 만들어 두었다. 시험이 운영의 한도를 깎아 먹는다는 것은 2편의 데이터 소스에서도, 여기서도 똑같이 배웠다.

시리즈를 마치며

돌아보면 대부분의 시간은 기능이 아니라 "실패했을 때 어떻게 되는가"에 썼다. 수집이 실패하면 직전 값, LLM 이 실패하면 템플릿, 음성 합성이 막히면 브라우저 음성. 매일 아침 쓰는 물건은 가끔 덜 똑똑해도 되지만 멈추면 안 된다.

시리즈 전체 목록은 여기, 서버에 올리는 과정은 홈서버-따라하기에 있다.

포트 하나 안 열고 홈 서버 운영하기 (0) 전체 구성

남는 PC 한 대로 개인 서비스 여러 개를 운영하고 있다. 공유기 포트 포워딩도, 고정 IP 도 없이 돌아간다. 따라 해 보고 싶다는 사람이 있어서 구성을 몇 편으로 나눠 적는다. 이 글은 전체 그림이고, 각 편은 홈서버-따라하기 라벨에 모여 있다.

전제

  • 서버는 회사나 집의 공유기·방화벽 뒤에 있다. 바깥에서 들어오는 포트는 하나도 열 수 없다고 가정한다.
  • 도메인 하나를 갖고 있고, DNS 는 Cloudflare 에 맡겼다.
  • 서버는 Ubuntu, 서비스는 전부 Docker 컨테이너다.

전체 구성

방문자 ──HTTPS──▶ Cloudflare 엣지 ◀──(바깥으로 나가는 연결)── cloudflared (서버)
                       │                                          │
                 Access (로그인)                         127.0.0.1:포트 로 전달
                                                                  │
                                              Docker 컨테이너들 (루프백에만 바인딩)

핵심은 방향이다. 서버가 Cloudflare 로 먼저 연결을 걸고, 방문자의 요청은 그 연결을 타고 들어온다. 그래서 인바운드 포트가 필요 없다.

편별 내용

  1. Cloudflare Tunnel 로 도메인 연결하기 — 터널 만들기, 서브도메인마다 서비스 연결, 다른 도메인을 붙일 때 주의할 점
  2. git push 로 배포하기 — Portainer git 스택, 이미지 빌드, 시크릿 파일 다루기
  3. 관리 화면 잠그기 — Cloudflare Access 이메일 코드 로그인, 오리진을 루프백에만 여는 이유
  4. 백업과 정기 정비 — 암호화 원격 백업, 업그레이드 전후 점검

이 구성의 장단점

  • 장점: 공인 IP·포트 포워딩이 필요 없다. 서버 IP 가 바뀌어도 상관없다. HTTPS 인증서를 직접 관리하지 않는다.
  • 단점: 모든 트래픽이 Cloudflare 를 지난다. HTTP(S) 가 아닌 프로토콜은 따로 손이 간다. 서버로 SSH 접속할 길은 별도로 마련해야 한다(나는 메시 VPN 을 쓴다).

겪은 문제들

운영하면서 밟은 함정은 따로 글로 적고 있다. 배포 쪽은 Portainer, 컨테이너 쪽은 Docker, 인증 쪽은 Cloudflare 라벨에서 볼 수 있다.

포트 하나 안 열고 홈 서버 운영하기 (1) Cloudflare Tunnel 로 도메인 연결

홈서버-따라하기 시리즈 1편. 서버에서 돌고 있는 웹 서비스를 app.example.com 같은 주소로 공개한다. 공유기 설정은 건드리지 않는다.

1. cloudflared 설치와 로그인

서버에 cloudflared 를 설치한 뒤 로그인한다. 출력된 주소를 브라우저에서 열어 사용할 도메인(존)을 고르면 인증서 파일이 ~/.cloudflared/cert.pem 에 저장된다.

cloudflared tunnel login

2. 터널 만들기

cloudflared tunnel create home
# 터널 ID 와 자격 증명 파일(~/.cloudflared/<TUNNEL_ID>.json) 이 생긴다

3. 설정 파일

/etc/cloudflared/config.yml 에 "어느 호스트 이름을 서버의 어느 포트로 보낼지"를 적는다.

tunnel: <TUNNEL_ID>
credentials-file: /etc/cloudflared/<TUNNEL_ID>.json

ingress:
  - hostname: app.example.com
    service: http://localhost:8081
  - hostname: wiki.example.com
    service: http://localhost:8082
  - service: http_status:404

마지막의 404 규칙은 필수다. 어느 규칙에도 맞지 않는 요청을 받을 기본값이 있어야 한다.

4. DNS 연결과 서비스 등록

cloudflared tunnel route dns <TUNNEL_ID> app.example.com
sudo cloudflared service install
sudo systemctl restart cloudflared

route dns 는 해당 이름의 CNAME 을 터널 주소로 만들어 준다. 이후 서비스를 추가할 때는 ① ingress 에 한 줄 추가 ② route dns ③ 재시작, 세 단계만 반복하면 된다.

겪은 함정

  • route dns 는 터널 이름 대신 ID 로. 내 환경에서는 이름으로 지정하면 "Tunnel not found" 가 났다. ID 로 하면 된다.
  • 로그인한 존이 아닌 도메인에는 route dns 를 쓰지 않는다. cert.pem 은 로그인할 때 고른 존 전용이다. 두 번째 도메인 other.example.net 을 붙이려고 같은 명령을 쓰면, 엉뚱하게 첫 번째 존 아래에 other.example.net.example.com 같은 레코드가 생긴다. 이럴 때는 대시보드에서 그 도메인의 DNS 에 CNAME 을 직접 추가한다. 대상은 <TUNNEL_ID>.cfargotunnel.com, 프록시는 켠다. ingress 의 hostname 은 평소처럼 추가하면 된다.
  • 다른 Cloudflare 계정의 도메인은 그 계정의 터널이 필요하다. 터널 CNAME 은 터널을 소유한 계정 안에서만 동작한다. 지인의 도메인을 내 서버에 붙여야 했을 때는 그 계정으로 터널을 하나 더 만들고, 설정 파일과 systemd 서비스를 따로 두어 cloudflared 를 두 개 돌렸다. tunnel login 은 항상 같은 경로에 인증서를 쓰므로, 기존 cert.pem 을 잠시 백업해 두었다가 새 인증서를 다른 폴더로 옮긴 뒤 되돌려 놓는다. 실행 중인 터널은 자격 증명 파일로 동작하므로 그동안 끊기지 않는다.

확인

systemctl status cloudflared
curl -I https://app.example.com

다음 편에서는 이 포트 뒤에서 돌아갈 컨테이너를 git push 만으로 배포하는 방법을 다룬다. 시리즈 전체 목록은 여기.

포트 하나 안 열고 홈 서버 운영하기 (2) git push 로 배포하기

홈서버-따라하기 시리즈 2편. 서버에 접속해서 명령을 치지 않고, 저장소에 push 하면 몇 분 안에 배포되게 만든다. 도구는 Portainer 의 git 스택이다.

구성

  • 앱마다 git 저장소 하나. 루트에 Dockerfile 과 docker-compose.yml.
  • Portainer 에서 "Repository" 방식으로 스택을 만들고 저장소 주소를 준다.
  • 자동 갱신(폴링)을 켜면 Portainer 가 주기적으로 새 커밋을 확인해 다시 배포한다. 나는 5분으로 두었다.

compose 파일 예시

services:
  app:
    build: .
    pull_policy: build
    restart: unless-stopped
    ports:
      - "127.0.0.1:8081:3000"
    environment:
      TZ: Asia/Seoul
      ENV_FILE: /config/.env
    volumes:
      - /home/user/app.env:/config/.env:ro
      - state:/state
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }

volumes:
  state:

왜 이렇게 썼나

  • build: . — 소스 폴더 마운트는 안 된다. Portainer 는 저장소를 자기 데이터 볼륨 안에 내려받는다. 그 경로는 호스트의 일반 폴더가 아니라서 ./src:/app 같은 바인드 마운트가 동작하지 않는다. 코드는 이미지 안에 넣어야 한다.
  • pull_policy: build — 없으면 재배포가 헛돈다. 이미지가 이미 있으면 compose 는 다시 빌드하지 않는다. 이 한 줄이 빠지면 배포 기록은 성공인데 코드는 그대로다.
  • 포트는 127.0.0.1 에만. 바깥 노출은 1편의 터널이 맡는다. 컨테이너 포트를 LAN 에 열 이유가 없다.
  • 시크릿은 호스트의 고정 경로에 두고 읽기 전용으로 마운트. 저장소에는 .env.example 만 올린다. Portainer 스택은 호스트의 env_file 을 읽지 못하므로, 파일을 컨테이너에 마운트하고 앱이 시작할 때 그 경로를 읽게 했다.
  • 상태는 이름 있는 볼륨에. 컨테이너를 다시 만들어도 남아야 하는 데이터만 넣는다.

Dockerfile 요령

FROM node:24-alpine
WORKDIR /app
COPY package.json server.js ./
COPY lib ./lib
COPY public ./public
ENV NODE_OPTIONS="--dns-result-order=ipv4first"
HEALTHCHECK CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1
CMD ["node", "server.js"]
  • COPY . . 대신 필요한 것만 적는다. git 스택의 체크아웃에는 저장소에서 지운 파일이 남아 있을 수 있어서, 통째로 복사하면 지운 파일이 이미지에 되살아난다.
  • 내 서버의 컨테이너 네트워크에는 IPv6 경로가 없어서, 외부 API 호출이 IPv6 주소를 먼저 시도하다 지연되는 일이 있었다. IPv4 우선 옵션으로 해결했다.

비공개 저장소라면

Portainer 가 저장소를 읽을 수 있어야 한다. 읽기 전용 액세스 토큰을 발급해 스택에 등록한다. 시크릿이 전혀 없는 정적 사이트 같은 것은 저장소를 공개로 두면 인증 자체가 필요 없다.

배포가 됐는지 확인

배포 기록의 "성공"보다 결과물을 본다. 앱이 버전이나 빌드 시각을 /healthz 로 내보내게 해 두면 편하다.

curl -s http://127.0.0.1:8081/healthz

이 과정에서 밟은 함정 두 가지는 Portainer 라벨의 글에 자세히 적었다. 다음 편은 관리 화면을 로그인 뒤로 숨기는 방법이다. 시리즈 전체 목록은 여기.

포트 하나 안 열고 홈 서버 운영하기 (3) 관리 화면 잠그기

홈서버-따라하기 시리즈 3편. 터널로 공개한 서비스 중 나만 써야 하는 것(관리 화면, 개인 대시보드)을 로그인 뒤로 숨긴다. 앱에 로그인 기능을 만들지 않고 Cloudflare Access 를 앞에 세운다.

Access 가 하는 일

요청이 서버에 닿기 전에 Cloudflare 엣지에서 "이 사람이 들어와도 되는가"를 확인한다. 통과하지 못한 요청은 서버까지 오지 않는다. 앱은 로그인 화면도, 비밀번호 저장도 필요 없다.

설정 순서

  1. Zero Trust 대시보드에서 로그인 수단에 One-time PIN(이메일 일회용 코드)을 추가한다. 기본 수단만 있으면 이메일 입력란이 나오지 않는다.
  2. Access → Applications 에서 Self-hosted 애플리케이션을 만들고 보호할 주소(admin.example.com)를 넣는다.
  3. 로그인 수단은 One-time PIN 하나만 선택하고 Instant authentication 을 켠다.
  4. 정책을 만든다. Action 은 Allow, Include 는 Emails 에 내 메일 주소.

이제 그 주소로 들어가면 메일 주소를 묻고, 허용한 주소로만 코드가 간다.

같은 사이트의 일부만 잠그기

공개 사이트의 관리 화면만 잠그고 싶다면 호스트 이름을 나누는 편이 깔끔하다. 같은 컨테이너를 www.example.com 과 admin.example.com 두 이름으로 터널에 연결하고, Access 는 admin 쪽에만 건다. 앱은 Host 헤더를 보고 관리 경로를 admin 호스트에서만 응답한다.

오리진을 직접 못 두드리게

Access 는 엣지에서만 막는다. 서버 안에서는 Host 헤더만 맞추면 인증 없이 응답이 온다.

curl -H 'Host: admin.example.com' http://127.0.0.1:8081/

배포 직후 점검에는 편하지만, 이 포트가 LAN 이나 인터넷에 열려 있다면 누구나 같은 방법으로 우회할 수 있다는 뜻이다. 그래서 두 가지를 지킨다.

  • 컨테이너 포트는 127.0.0.1 에만 바인딩한다(2편).
  • 민감한 앱은 Access 가 요청에 붙여 주는 JWT(Cf-Access-Jwt-Assertion 헤더)를 서버에서 검증한다. 서명 키는 팀 도메인의 공개 인증서 주소에서 받고, 대상(aud) 값이 내 애플리케이션의 것인지 확인한다. 이렇게 하면 터널을 거치지 않은 요청은 앱이 스스로 거절한다.

서비스가 많아지면

사용자가 여럿이거나 앱마다 권한이 달라야 하면 자체 SSO(나는 Authentik)를 두는 방법도 있다. 프록시 방식으로 앱 앞에 세우면 앱을 고치지 않고 로그인을 붙일 수 있다. 다만 혼자 쓰는 서비스 몇 개라면 Access 의 이메일 코드로 충분했다.

설정 중에 막혔던 부분은 Cloudflare 라벨의 글에 따로 적었다. 다음 편은 백업과 정기 정비다. 시리즈 전체 목록은 여기.

포트 하나 안 열고 홈 서버 운영하기 (4) 백업과 정기 정비

홈서버-따라하기 시리즈 4편. 서버를 세우는 것보다 계속 굴리는 쪽이 일이 많다. 매일 도는 백업과, 한두 달에 한 번 하는 정비 절차를 적는다.

매일: 암호화 원격 백업

백업은 서버 밖에 있어야 의미가 있다. rclone 의 crypt 리모트를 쓰면 클라우드 드라이브에 올리되 내용과 파일 이름을 암호화할 수 있다.

  • DB 는 파일 복사가 아니라 덤프로 뜬다: docker exec <db 컨테이너> sh -c 'pg_dump -U "$POSTGRES_USER" -Fc "$POSTGRES_DB"'
  • 볼륨은 임시 컨테이너로 묶는다: docker run --rm -v <볼륨>:/data:ro -v /backup:/out alpine tar czf /out/data.tgz -C /data .
  • 모은 것을 rclone sync /backup crypt-remote:server 로 올린다.
  • 실행은 cron 대신 systemd timer 로 걸었다. 실패가 systemctl status 와 저널에 남아서 확인이 쉽다.

백업 스크립트를 Windows 에서 편집했다면 줄 끝이 LF 인지 꼭 확인한다. CRLF 면 예약 실행에서만 조용히 실패한다.

정비 전

docker ps --format '{{.Names}}\t{{.Image}}\t{{.Status}}' | sort > ~/pre-upgrade.txt
docker info --format '{{.LiveRestoreEnabled}}'

두 번째 명령이 false 면 Docker 패키지가 갱신될 때 컨테이너가 전부 재시작된다. 그 시간대에 해도 되는지 먼저 판단한다.

업그레이드

sudo systemd-run --unit=apt-upgrade \
  sh -c 'apt-get update && apt-get -y upgrade > /var/log/apt-upgrade.log 2>&1'

ssh 가 끊겨도 계속 돌도록 별도 유닛으로 실행하고 로그를 파일로 남긴다. 커널이 올라갔으면 재부팅한다. 내 서버는 재부팅 뒤 1분 남짓이면 원격 접속이 돌아왔다.

정비 후

docker ps --format '{{.Names}}\t{{.Image}}' | sort | diff <(cut -f1,2 ~/pre-upgrade.txt) - && echo OK
systemctl is-active cloudflared
curl -sI https://app.example.com | head -1

컨테이너 목록이 정비 전과 같은지, 터널이 살아 있는지, 바깥에서 실제로 열리는지 세 가지를 본다.

개별 서비스 업그레이드 순서

  1. 컨테이너를 멈춘다.
  2. 볼륨을 tar 로 백업한다(DB 는 덤프).
  3. 현재 이미지에 롤백용 태그를 붙여 둔다: docker tag app:latest app:rollback-1.2.3
  4. 새 버전으로 올린다. 이미지 태그는 latest 대신 버전을 고정해 두면 "언제 무엇이 바뀌었는지"가 분명해진다.
  5. 확인이 끝나면 예전 컨테이너는 지운다. 이름만 바꿔 남겨 두면 restart=always 때문에 다음 데몬 재시작 때 되살아나서, 이미 마이그레이션된 데이터에 구버전이 붙을 수 있다. 롤백 자료는 이미지 태그와 백업 파일로 충분하다.

기록

정비할 때마다 날짜, 올라간 버전, 평소와 달랐던 점을 한 줄씩 남긴다. "재부팅 직후 이 컨테이너는 몇 번 재시작하다 붙는다 — 정상" 같은 메모가 다음 정비 때 시간을 아껴 준다. 단, 확인한 사실과 추측은 구분해서 적는다. 추측을 사실처럼 적어 두었다가 낭패를 본 이야기는 운영 라벨의 글에 있다.

시리즈 전체 목록은 여기.

Authentik API 로 만든 OAuth2 Provider 가 invalid_request 를 낼 때 — grant_types 빈 배열

Authentik 의 관리 화면 대신 REST API 로 OAuth2 Provider 를 만들었더니, 로그인 시도마다 클라이언트가 invalid_request 를 받았다. 설정을 아무리 봐도 틀린 데가 없었는데, API 로 만들 때만 비어 있는 필드가 하나 있었다.

증상

  • POST /api/v3/providers/oauth2/ 로 Provider 를 만들고 Application 에 연결했다(Authentik 2026.5).
  • 앱에서 로그인 버튼을 누르면 authorize 단계에서 곧바로 콜백으로 돌아오며 error=invalid_request, 설명은 "The request is otherwise malformed".
  • client_id, redirect URI, scope 는 전부 맞다. 관리 화면에서 같은 값으로 만든 Provider 는 잘 된다.

원인

Authentik 서버 로그에는 클라이언트가 받은 것과 다른, 더 구체적인 메시지가 있었다.

Invalid grant_type for provider

API 로 만든 Provider 를 조회해 보니 grant_types 가 빈 배열이었다. 관리 화면은 기본값을 채워 주지만, API 는 요청 본문에 없으면 비워 둔 채로 만든다. 허용된 grant 가 하나도 없으니 authorization code 요청이 거부되고, 클라이언트에는 뭉뚱그린 invalid_request 로 전달된 것이다.

해결

생성 요청 본문에 넣거나, 이미 만든 Provider 를 PATCH 한다.

PATCH /api/v3/providers/oauth2/<pk>/
{
  "grant_types": ["authorization_code", "refresh_token"]
}

API 로 만들 때 함께 챙겨야 했던 필드들도 적어 둔다.

  • redirect_uris 는 문자열이 아니라 객체 배열이다: [{"matching_mode": "strict", "url": "https://app.example.com/callback"}]
  • authorization_flow, invalidation_flow, signing_key 는 각각의 pk 를 넣어야 한다. 기존에 잘 동작하는 Provider 를 GET 해서 값을 참고하면 빠르다.
  • property_mappings 에 openid, email, profile scope 매핑의 pk 를 넣는다. 그룹 정보는 profile scope 에 들어 있다.

정리

OAuth 오류는 클라이언트에 전달되는 메시지가 일부러 모호하다. invalid_request 를 받으면 요청을 의심하기 전에 인증 서버 쪽 로그를 먼저 본다. 그리고 UI 로 만든 것과 API 로 만든 것이 다르게 동작하면, 두 객체를 GET 해서 나란히 비교하는 것이 가장 빠르다.

Cloudflare Access 로그인 화면에 이메일 OTP 가 안 뜰 때

관리자 페이지를 Cloudflare Access 로 막고 "허용한 이메일로 일회용 코드를 받아 로그인"하게 하려 했는데, 로그인 화면에 이메일 입력란이 나오지 않았다. 정책이 아니라 로그인 수단 쪽 설정이 빠져 있었다.

증상

  • Zero Trust 에서 Self-hosted 애플리케이션을 만들고, 정책에 허용할 이메일을 넣었다.
  • 보호한 주소로 들어가면 Access 화면은 뜨는데, 이메일로 코드를 받는 선택지가 없다.

원인

Access 의 정책은 "누가 들어올 수 있는가"를 정하고, 로그인 수단(Identity provider)은 "어떻게 본인임을 증명하는가"를 정한다. 둘은 별개다. 내 계정에는 기본 로그인 수단만 등록돼 있었고, 이메일 일회용 코드(One-time PIN)는 목록에 없었다. 등록되지 않은 수단은 로그인 화면에 나올 수 없다.

해결

  1. Zero Trust 대시보드의 Integrations → Identity providers 에서 One-time PIN 을 추가한다.
  2. 애플리케이션 설정에서 "모든 로그인 수단 허용"을 끄고 One-time PIN 만 선택한다.
  3. 로그인 수단이 하나뿐이면 Instant authentication 을 켠다. 수단을 고르는 화면을 건너뛰고 바로 이메일 입력으로 간다.
  4. 정책은 Include 에 Emails 셀렉터로 허용할 주소를 나열한다.

참고로 Zero Trust 는 무료 플랜으로도 쓸 수 있었다.

서버에서 직접 테스트하기

Access 는 Cloudflare 엣지에서만 막는다. 오리진 서버 안에서는 Host 헤더만 맞춰 주면 인증 없이 응답을 볼 수 있어서, 배포 직후 동작 확인에 편하다.

curl -H 'Host: admin.example.com' http://127.0.0.1:8080/

뒤집어 말하면, 오리진이 인터넷에 직접 열려 있다면 Access 를 우회할 수 있다는 뜻이다. 오리진은 터널 뒤에 두거나 루프백에만 바인딩하고, 가능하면 앱에서 Access 가 붙여 주는 JWT 를 검증해 이중으로 막는다.

정리

Access 에서 원하는 로그인 방식이 화면에 안 보이면 정책을 들여다보기 전에 Identity providers 목록부터 확인한다. 정책은 통과 조건이고, 로그인 수단은 따로 등록해야 한다.

GitOps 자동 배포가 "성공"인데 코드가 안 바뀔 때 — compose 의 pull_policy: build

push 만 하면 배포되도록 Portainer 의 GitOps 폴링을 켰는데, 배포 기록은 매번 성공인데도 사이트는 예전 코드 그대로였다. compose 파일에 한 줄이 빠져 있었다.

증상

  • 스택은 git 저장소 기반이고, 이미지는 레지스트리에서 받지 않고 build: . 로 서버에서 직접 빌드한다.
  • 폴링 주기마다 새 커밋을 잘 감지하고 "재배포 성공"으로 표시된다.
  • 그런데 컨테이너 안의 코드는 바뀌지 않는다. 컨테이너가 재생성조차 되지 않는 경우도 있다.

원인

docker compose up 은 서비스에 쓸 이미지가 이미 로컬에 있으면 다시 빌드하지 않는다. 처음 배포할 때 만든 이미지가 같은 이름으로 남아 있으니, 소스가 바뀌어도 compose 입장에서는 "이미지 있음, 할 일 없음"이다. 손으로 배포할 때는 --build 를 붙여서 몰랐던 문제가, 자동 재배포에서는 그 옵션이 붙지 않으면서 드러난 것이다.

해결

서비스에 pull_policy: build 를 넣는다. 이 값이면 compose 가 올릴 때마다 이미지를 빌드한다(바뀐 게 없으면 레이어 캐시 덕분에 금방 끝난다).

services:
  app:
    build: .
    pull_policy: build
    restart: unless-stopped

확인 방법

배포가 실제로 반영됐는지는 배포 기록이 아니라 결과물로 확인하는 편이 확실하다.

  • 이미지 생성 시각: docker image inspect <이미지> --format '{{.Created}}'
  • 앱이 빌드 시점의 커밋 해시나 버전을 응답에 내보내도록 해 두고 그것을 본다.

정리

레지스트리 없이 서버에서 빌드하는 구성으로 GitOps 를 쓸 때는 pull_policy: build 가 사실상 필수다. "성공"이라는 표시는 compose 명령이 오류 없이 끝났다는 뜻일 뿐, 새 코드가 올라갔다는 뜻이 아니다.

Windows 에서 올린 셸 스크립트가 cron 에서만 조용히 실패할 때 — CRLF

Windows 에서 작성해 서버에 올린 백업 스크립트가 cron 에서 한 번도 돌지 않았다. 손으로 실행하면 될 때도 있어서 원인을 찾는 데 오래 걸렸다. 범인은 줄 끝의 CRLF 였다.

증상

  • crontab 에 등록한 .sh 가 정해진 시각에 아무 결과도 남기지 않는다.
  • cron 메일이나 로그를 보면 not found 한 줄뿐이다. 파일은 분명히 있고 실행 권한도 있다.
  • bash script.sh 로 직접 실행하면 일부는 돌아가서, 스크립트 자체는 멀쩡하다고 착각하게 된다.

원인

Windows 편집기는 줄 끝을 CRLF(\r\n)로 저장한다. 그러면 첫 줄이 이렇게 된다.

#!/bin/bash\r

커널은 /bin/bash\r 이라는 이름의 인터프리터를 찾는데, 그런 파일은 없다. 그래서 "스크립트가 없다"가 아니라 "인터프리터가 없다"는 뜻의 not found 가 나온다. 메시지만 보면 스크립트 경로가 틀린 것처럼 읽혀서 헷갈린다.

bash script.sh 로 실행하면 shebang 을 쓰지 않으니 시작은 되지만, 각 줄 끝에 \r 이 붙은 채로 해석되어 변수 값이나 경로 끝에 보이지 않는 문자가 섞인다. Python 스크립트도 shebang 으로 실행하면 같은 이유로 실패한다.

확인

file script.sh          # "with CRLF line terminators" 가 보이면 당첨
head -1 script.sh | od -c | head -2   # \r \n 확인

해결

당장은 서버에서 변환한다.

sed -i 's/\r$//' script.sh

근본적으로는 저장소에서 줄 끝을 고정한다. .gitattributes 한 줄이면 Windows 에서 체크아웃해도 LF 로 유지된다.

* text=auto eol=lf

이미 CRLF 로 커밋된 파일이 있다면 설정을 넣은 뒤 git add --renormalize . 로 한 번 정리한다.

정리

cron 은 실패를 눈앞에 보여 주지 않는다. Windows 에서 만든 스크립트를 서버에 올린다면 .gitattributes 로 LF 를 강제해 두고, 새 cron 작업은 등록 직후 로그로 실제 실행 여부를 한 번 확인하는 습관이 싸게 먹힌다.

반도체 주식을 보기 전에 그려 두는 지도 — 설계, 파운드리, 메모리, 장비, 소재, 후공정

"반도체주"라고 한 묶음으로 부르지만, 그 안의 회사들은 하는 일도 돈 버는 방식도 전혀 다르다. 같은 뉴스에 어떤 회사는 오르고 어떤 회사는 내리는 이유가 여기에 있다. 종목을 들여다보기 전에 머릿속에 그려 두면 좋은 지도를 정리한...