AI 계정 다음에 필요한 교육 운영

한눈에 보기

  • 아이슬란드 교육아동부와의 시범사업
  • 계정과 자료·연수·지원망 묶기
  • 학원 도입도 지원 구조부터 설계하기

AI 계정은 연수·자료·질문할 지원망과 함께 운영 구조로 설계해야 합니다. Anthropic과 아이슬란드 교육아동부가 2025년 11월 4일 발표한 국가 단위 시범사업도 전국 수백 명의 교사에게 Claude와 교육 자료, 연수 자료, 전담 지원 네트워크를 함께 제공하는 방식이었습니다. 국내 학원에 그대로 적용할 사업은 아니지만, 계정 이후 지원 구조를 설계할 때 참고할 수 있습니다.

아이슬란드 시범사업은 무엇을 묶었나

사업의 목적은 AI가 학교와 교사의 수업 준비, 수업 지도, 학생 학습에 어떻게 도움이 되는지 탐색하는 것입니다. 특정 성적 향상을 약속하기보다 교사의 필요를 길잡이로 삼겠다는 방향을 내세웠고, 교육아동부 장관도 교사의 필요를 중심에 두겠다는 취지로 말했습니다.

언어와 문화의 조건도 발표에 들어 있습니다. Anthropic 측은 아이슬란드어 보호를 함께 언급했고, 아이슬란드어를 인식하는 환경을 고려했습니다. 계정만 나눠 주는 방식이 아니라 교사가 쓸 자료와 연수, 막힐 때 물어볼 지원망을 한 번에 준비한 점이 이 사업의 설계입니다.

검색 사례와 학원 도입의 간격

같은 발표에는 유럽의회 기록보관 부서가 210만 건 이상의 문서를 다루며 검색 시간을 80% 단축했다는 사례도 나옵니다. 이 수치는 아이슬란드 시범사업의 성과가 아니라 별도의 활용 사례입니다. 서로 다른 사례를 한 사업의 결과처럼 합치지 않아야 합니다.

학원에서 참고할 대목은 계정 발급 이후의 운영입니다. 수업 준비에서 반복되는 업무를 고르고, 연수에서 직접 자료를 만들어 보고, 잘 된 양식을 공유하고, 막히면 도움을 요청할 담당자를 정하는 순서입니다. 실제 학생 정보와 수업 자료를 다룰 때는 접근 권한과 보관 방식도 함께 정해야 합니다.

AI 도입 뒤에 붙일 지원 구조

  • 계정 발급과 함께 첫 실습에서 만들 결과물을 정합니다.
  • 강사들이 공유할 수업 자료와 프롬프트의 위치를 정합니다.
  • AI가 읽을 자료의 범위와 개인정보 처리 기준을 적습니다.
  • 사용 중 막혔을 때 질문할 담당자와 답변 방식을 정합니다.
  • 한 달 뒤 실제로 쓰인 업무와 남은 불편을 검토합니다.

자주 묻는 질문

아이슬란드 사업을 한국 학원에 그대로 적용할 수 있나요?

그 사업은 아이슬란드 교육아동부와의 국가 시범사업입니다. 국내 학원은 국가 제도나 제공 조건을 복사하기보다 계정, 연수, 자료, 지원을 자신의 규모에 맞게 설계해야 합니다.

아이슬란드어 보호가 학원에 어떤 의미가 있나요?

AI를 쓸 때도 수업의 언어와 지역 맥락을 지키는 조건을 먼저 정하라는 뜻으로 읽을 수 있습니다. 한국 학원이라면 학생에게 맞는 표현과 교사의 수업 의도를 검토 기준에 넣을 수 있습니다.

지원 담당자가 꼭 필요한가요?

도입 초기에는 질문이 어디로 가는지 정해 두는 편이 좋습니다. 담당자는 모든 문제를 대신 해결하기보다 자료와 권한, 재현 절차를 함께 정리하는 역할을 맡으면 됩니다.

출처

이 글은 anthropic.com 원문을 학원 운영 관점에서 요약·해설한 글이며 번역이 아닙니다.

AI가 푼 숙제에서 배움을 확인하는 법

한눈에 보기

  • 전 세계 대학 이메일 계정의 익명 대화 분석
  • 네 가지 사용 패턴과 블룸 분류
  • 한국 초중등에 일반화하지 않기

AI가 푼 숙제에서 배움을 확인하려면 결과물만 보지 말고 근거·풀이·설명 과정을 함께 받으세요. Anthropic이 2025년 4월 8일 발표한 Education Report는 전 세계 대학 이메일 계정의 익명 대화 약 100만 건 가운데 학업 관련 574,740건을 분석한 자료로, 한국 초중등 교실의 부정행위를 직접 판정하는 조사는 아닙니다.

대화는 네 가지 사용법으로 나뉘었다

보고서는 학생과 AI의 상호작용을 직접 문제 해결, 직접 결과물 생성, 협력 문제 해결, 협력 결과물 생성으로 나눕니다. 각 패턴은 대략 23~29% 범위였고, 직접 문제 해결과 직접 결과물 생성을 합친 직접형은 약 47%였습니다. 협력형이라고 해서 항상 학습이 일어난다고 단정할 수도 없습니다.

Claude의 응답이 어떤 사고 수준을 대신했는지도 블룸 분류로 살폈습니다. 만들기가 39.8%, 분석하기가 30.2%, 적용이 10.9%, 이해가 10.0%, 기억이 1.8%로 제시됩니다. 숫자가 보여주는 것은 학생이 AI를 어려운 과제에도 사용한다는 점이지, 그 결과가 곧 학습이나 부정행위라는 뜻은 아닙니다.

숫자를 평가 규칙으로 바꾸지 않기

대화 기록만으로는 집에서 치르는 시험 답을 구한 것인지, 연습문제를 점검한 것인지 알 수 없습니다. 협력형 대화도 확률·통계 숙제를 여러 차례 주고받으며 결국 사고를 AI에 넘기는 경우가 있을 수 있다고 보고서는 지적합니다. 맥락과 과제 규칙을 함께 보지 않으면 분류가 판정으로 바뀝니다.

학원에서는 결과물만 제출받는 과제에 근거 설명을 붙일 수 있습니다. 독해라면 답의 근거 문장을 찾게 하고, 영작이라면 고친 이유를 말하게 하며, 필요하면 짧은 구두 확인을 둡니다. 이 방법은 AI 사용을 자동으로 적발한다는 약속이 아니라, 학생이 무엇을 이해했는지 확인하는 평가 설계입니다.

과정 중심 평가를 설계할 때

  • 결과물과 함께 핵심 근거 또는 풀이 과정을 제출하게 합니다.
  • AI 사용 허용 범위와 학생이 직접 해야 할 부분을 미리 씁니다.
  • 구두 설명, 초안 비교, 근거 표시 중 과목에 맞는 방법을 고릅니다.
  • 대화 분류 수치를 한국 초중등 전체의 비율처럼 사용하지 않습니다.
  • 평가 후 학생이 막힌 개념을 다음 수업 자료에 반영합니다.

자주 묻는 질문

이 보고서로 AI 부정행위를 판정할 수 있나요?

판정할 수 없습니다. 익명 대화의 목적과 과제 상황을 모두 알 수 없으므로, 개별 학생의 행동을 추정하는 근거로 쓰면 안 됩니다.

협력형 사용은 학습에 더 좋은가요?

협력형이라는 이름만으로 학습 효과를 보장하지 않습니다. 학생이 근거를 설명하고 자신의 판단을 남겼는지까지 확인해야 합니다.

한국 학원도 보고서 비율을 적용할 수 있나요?

보고서는 전 세계 대학 이메일 계정과 대학생 대화를 대상으로 했습니다. 학원은 같은 비율을 적용하기보다 자신의 과제와 학생 수준에 맞는 과정 확인 방법을 설계해야 합니다.

출처

이 글은 anthropic.com 원문을 학원 운영 관점에서 요약·해설한 글이며 번역이 아닙니다.

교사가 직접 만드는 AI 학습 도구

한눈에 보기

  • 교사를 공동 설계자로 부르기
  • 세계 교육 프로그램의 세 가지 활동
  • 학원에서는 학생의 어려움부터 출발하기

교사는 학생이 반복해서 막히는 지점을 바탕으로 작은 AI 학습 도구를 직접 설계해 볼 수 있습니다. Anthropic과 Teach For All이 2026년 1월 21일 발표한 글로벌 교육자 프로그램 사례를 학원에 그대로 적용하는 것이 아니라, 교사가 목표와 결과물을 검토하는 절차로 한정해 참고하세요.

교사를 공동 설계자로 부른 구조

이 프로그램의 AI Literacy & Creator Collective에는 63개국의 교사와 동문 10만 명 이상이 참여하고, 이들이 가르치는 학생은 150만 명 이상이라고 안내합니다. 교사를 단순한 사용자보다 공동 설계자(co-architects)로 부르는 것이 특징입니다. 교실을 아는 사람이 도구의 문제 정의에 들어와야 한다는 전제입니다.

활동은 세 갈래로 소개됩니다. AI Fluency Learning Series는 라이브 6회 학습 시리즈로, 2025년 11월 첫 시리즈에는 530명 이상이 참여했습니다. Claude Connect는 60개국 이상의 교사가 매일 사례를 나누는 공간이고, Claude Lab은 더 나아가고 싶은 교사에게 Claude Pro를 제공해 실험하게 하고 월별 오피스아워를 엽니다. 발표 나흘 만에 200건 이상 지원이 들어왔다는 수치도 프로그램의 초기 반응으로 제시됩니다.

사례가 학원에 주는 힌트

라이베리아의 한 교사는 AI를 처음 접한 뒤 몇 주 만에 기후 교육용 인터랙티브 교과과정을 만들었습니다. 방글라데시에서는 6·7학년 학생의 절반 이상이 기초 연산을 어려워한다는 교사의 관찰을 바탕으로 보스전, 리더보드, 경험치가 있는 게임형 수학 앱을 설계했습니다. 아르헨티나에서는 교육과정과 연결된 디지털 워크스페이스를 만드는 사례가 소개됩니다.

이 사례가 한국 학원에 보장하는 도구나 성과가 있는 것은 아닙니다. 다만 교사가 학생의 오답과 막히는 지점을 먼저 알고, 그 정보를 작은 활동으로 바꾸는 순서는 참고할 수 있습니다. 학원에서도 완성된 플랫폼부터 찾기보다 특정 수업의 복습 활동 하나를 정하고, 교사가 직접 시안을 검토하는 방식으로 시작할 수 있습니다.

교사가 만들 학습 활동을 고르는 순서

  • 학생이 반복해서 막히는 개념을 수업 기록에서 찾습니다.
  • 활동의 목표와 대상 학년을 한 문장으로 적습니다.
  • AI에게 만들게 할 부분과 교사가 판단할 부분을 나눕니다.
  • 작은 퀴즈나 인터랙티브 자료로 먼저 시험합니다.
  • 학생 반응을 보고 다음 자료에 반영할 기준을 남깁니다.

자주 묻는 질문

교사가 프로그램에 참여하지 않아도 같은 결과를 낼 수 있나요?

원문 사례는 특정 국제 프로그램 안에서 나온 결과입니다. 국내 학원이 같은 결과를 기대하기보다, 교사가 학생의 어려움을 정의하고 작은 자료를 시험하는 절차부터 가져오는 편이 안전합니다.

게임형 학습 도구가 모든 수업에 맞나요?

그렇지 않습니다. 게임 형식은 기초 연산처럼 반복 연습이 필요한 내용에서 검토할 수 있지만, 학습 목표와 학생의 나이에 맞아야 합니다.

무엇부터 AI에게 맡기면 좋을까요?

학생이 자주 틀리는 개념을 짧은 연습 활동으로 바꾸는 일부터 시작할 수 있습니다. 교사는 목표와 난이도를 정하고, AI가 만든 자료는 수업 전에 직접 확인해야 합니다.

출처

이 글은 anthropic.com 원문을 학원 운영 관점에서 요약·해설한 글이며 번역이 아닙니다.

답 대신 질문하는 AI를 수업에 쓰는 법

한눈에 보기

  • 대학용 Learning mode의 질문 중심 응답
  • 답보다 근거와 개념 묻기
  • 학원에서는 질문을 만드는 수업 설계로 옮기기

수업에서 AI가 답을 대신 쓰지 않게 하려면 ‘답해줘’ 대신 학생에게 접근과 근거를 묻도록 지시하세요. Anthropic이 2025년 4월 2일 발표한 대학용 Claude for Education의 Learning mode 사례를, 국내 학원에서 바로 쓸 수 있는 기능이 아니라 질문 중심 수업 설계의 참고로 한정해 보세요.

Learning mode가 바꾸는 응답

학습 모드는 Projects 안에서 작동하며, “이 문제에 어떻게 접근하겠어?” 또는 “네 결론을 뒷받침하는 근거는 뭐야?”처럼 되묻는 응답을 지향합니다. 원문이 설명하는 방식은 네 가지입니다. 바로 답하는 대신 접근법을 묻고, 근거를 확인하고, 핵심 개념을 짚고, 보고서·학습 가이드·개요의 틀을 제공하는 것입니다.

이 기능은 Northeastern, LSE, Champlain College 같은 대학과의 전교 이용 계약 맥락에서 소개됐습니다. 따라서 대학용 제품과 국내 학원용 이용 조건을 혼동하지 않아야 합니다. 여기서 가져올 만한 점은 기능 이름보다 학생이 답을 만들기 전에 어떤 질문을 받아야 하는지 정하는 방식입니다.

학원 지시문을 질문형으로 바꾸기

강사에게 AI가 수업 자료를 바로 쓰게 하기보다 빈틈을 질문으로 찾게 하라고 지시할 수 있습니다. 학생의 독해 요약에는 “요약해 줘” 대신 “본문에서 근거가 되는 문장을 찾도록 질문해 줘”라고 요청합니다. 영작 첨삭도 완성 문장을 먼저 주기보다, 틀린 이유를 학생이 설명하게 하는 질문을 차례로 만들 수 있습니다.

이것은 특정 기능을 그대로 복제하는 작업이 아닙니다. 답변 속도보다 학생이 판단한 흔적을 남기는 것이 중요한 수업에서 쓸 수 있는 지시 방식입니다. 학년과 과목에 따라 질문의 난이도와 교사가 개입할 시점을 정하고, AI의 질문이 학습 목표에서 벗어나지 않는지 확인해야 합니다.

답 대신 질문을 설계할 수업

  • 학생이 먼저 시도할 문제와 AI가 질문할 범위를 정합니다.
  • 정답 대신 요구할 근거와 설명의 형태를 고릅니다.
  • 핵심 개념을 짚는 질문과 보고서 틀을 구분합니다.
  • 질문이 너무 어렵거나 넓을 때 교사가 개입할 시점을 둡니다.
  • 결과물보다 학생의 판단 과정을 볼 기록 방식을 정합니다.

자주 묻는 질문

Learning mode는 학원에서도 바로 쓸 수 있나요?

원문은 대학용 Claude for Education 기능으로 설명합니다. 국내 학원에서의 제공 여부나 조건은 별도로 확인해야 하며, 이 글은 질문 중심 수업 설계를 참고하자는 뜻입니다.

질문만 하면 학생이 반드시 스스로 풀게 되나요?

그렇지는 않습니다. 질문의 범위와 난이도가 적절한지, 학생이 실제 근거를 사용했는지 교사가 확인해야 합니다.

강사 업무에도 이 방식을 적용할 수 있나요?

가능합니다. 수업 자료를 만들어 달라고만 하기보다 빠진 개념이나 확인할 근거를 질문으로 찾게 하면 검토 단계에 활용할 수 있습니다.

출처

이 글은 anthropic.com 원문을 학원 운영 관점에서 요약·해설한 글이며 번역이 아닙니다.

우리 자료를 모르는 AI는 학원도 모른다

한눈에 보기

  • 대학 교육 도구 연결 사례
  • Canvas는 LTI, Wiley·Panopto는 MCP
  • 학원 자료의 구조화와 승인 절차

학원 자료를 AI에 연결하려면 먼저 자료의 목적·접근 권한·연결 방식을 나누어 정리하세요. Anthropic이 2025년 7월 9일 발표한 대학 교육 사례를 바탕으로 하되, Canvas의 LTI와 Wiley·Panopto의 MCP 사례를 국내 학원에 그대로 적용하지 않고 연결 원칙으로만 참고하세요.

대학 자료를 연결하는 방식은 도구마다 다르다

Panopto와 Wiley는 미리 만든 MCP 서버로 연결해 강의 녹화 자막과 동료 심사를 거친 학술 자료를 다루는 방식으로 소개됩니다. 반면 Canvas는 MCP가 아니라 LTI로 Canvas 강좌 안에서 Claude를 쓰도록 연결합니다. 연결 규격을 하나로 뭉뚱그리지 않고 자료의 위치와 제품의 권한에 맞춰 나눈 점이 중요합니다.

학생이 지난주 강의에서 어떤 내용이 나왔는지 묻고 자막에서 근거를 찾는 상황은 예시입니다. AI가 알고 있는 일반 지식이 아니라 기관이 가진 자료에서 답을 찾게 하려는 설계입니다. 학원에서도 수업 기록과 교재 진도, 복습 자료가 일정한 형식으로 쌓여 있어야 비슷한 질문을 검토할 수 있습니다.

개인정보와 승인 절차를 먼저 둔다

원문은 대화를 기본적으로 비공개로 두고 학습에서 제외한다고 설명합니다. 기관 자료를 요청하는 일에는 공식 승인이 필요하고, 사용자가 자료를 스스로 내보내는 기능도 기본 제한이 있다고 안내합니다. 이런 설정은 연결 기능보다 먼저 검토할 운영 조건입니다.

국내 학원에서는 출결, 상담, 성적 자료를 모두 연결하기보다 목적과 범위를 나누는 편이 낫습니다. 기록 양식을 통일하고, 누가 읽을 수 있는지 정한 뒤, 필요한 자료만 검색 대상으로 둡니다. 대학 발표 당시의 연결 방식이 한국 학원 프로그램에서도 작동한다고 단정하지 말고, 현재 사용하는 시스템의 연동 방법을 따로 확인해야 합니다.

학원 자료를 연결하기 전 정리할 것

  • 수업 기록과 교재 자료를 목적별 폴더와 양식으로 나눕니다.
  • Canvas식 LTI와 MCP를 같은 연결 방식으로 부르지 않습니다.
  • 학생 자료의 접근자와 공식 승인 절차를 정합니다.
  • AI가 답할 때 근거로 보여줄 원본 위치를 남깁니다.
  • 연결 전후에 자료가 학습이나 외부 공유에 쓰이는 조건을 확인합니다.

자주 묻는 질문

학원 자료도 MCP로 연결하면 되나요?

원문은 도구에 따라 연결 방식이 다르다고 설명합니다. Canvas는 LTI, Wiley와 Panopto는 미리 만든 MCP 서버 사례이므로 현재 학원 시스템에 맞는 방법을 별도로 확인해야 합니다.

학생 자료를 연결하면 개인정보 우려가 사라지나요?

그렇지 않습니다. 비공개와 학습 제외 설정이 있어도 누가 어떤 자료를 읽는지, 기관 승인이 필요한지 운영 규칙을 정해야 합니다.

자료를 먼저 정리해야 하는 이유는 무엇인가요?

AI가 답할 근거를 찾으려면 기록의 위치와 형식이 일정해야 합니다. 흩어진 메시지와 파일을 먼저 목적별로 정리하면 연결 범위를 좁히고 검토하기도 쉬워집니다.

출처

이 글은 anthropic.com 원문을 학원 운영 관점에서 요약·해설한 글이며 번역이 아닙니다.

AI에게 일을 맡길 때 먼저 정할 것

한눈에 보기

  • 완료 기준과 중단 조건을 먼저 정하기
  • 불필요한 생각 지시 줄이기
  • 체크리스트와 확인하지 못한 항목으로 긴 작업 관리

AI에게 긴 일을 맡길 때는 무엇이 끝인지, 언제 멈춰 사람에게 물을지를 먼저 정해야 합니다. Opus 5.5 가이드는 전체 일을 한 번에 맡기고 완료 기준을 말한 뒤, 결과를 확인하는 절차를 따로 두라고 권합니다.

학원에서도 “이거 좀 해줘”보다 “어디까지 끝나면 되는지”를 알려주는 편이 낫습니다. 숙제 피드백, 학부모 안내문, 워크북 검수처럼 중간 과정이 긴 업무일수록 시작 전에 끝을 정해야 합니다.

가이드의 팁 가운데 골라 볼 여섯 가지

비개발자에게 쓸모 있는 것만 추리면 이렇습니다. 첫째, 일을 통째로 맡기되 완료 기준과 멈춰 물을 조건을 함께 말합니다. 둘째, “신중히 생각해”, “단계별로 생각해” 같은 문구를 습관처럼 덧붙이지 않습니다. Opus 5.5는 답변 전에 생각하므로, Anthropic이 채팅 제품에서 해 본 테스트에서는 이 문구를 지웠을 때 답변이 더 빨리 시작됐고 품질 저하는 뚜렷하지 않았습니다.

셋째, 디자인을 요청할 때는 싫은 스타일을 구체적으로 적습니다. 원문 예시처럼 크림색·오프화이트 배경, 제목 속 이탤릭 강조 단어, 01/02/03 번호 라벨, 모노스페이스 라벨, 알약 모양 버튼을 원하지 않는다면 처음부터 목록으로 씁니다. 넷째, 긴 작업은 TASKS.md 같은 파일에 체크리스트를 적고 진행하면서 갱신합니다.

다섯째, 찾지 못했거나 직접 확인하지 못한 항목을 따로 보고하게 합니다. 여섯째, 차트·다이어그램·스크린샷은 숫자와 모양을 다시 옮겨 적지 말고 파일로 첨부합니다. Opus 5.5 자체 소개는 devlog 글에 정리했습니다.

AI에게 일을 맡길 때 무엇부터 정할까요?

첫 메시지에 전체 업무, 완료선, 질문할 조건을 넣어보세요. 예를 들어 “숙제 피드백 초안을 작성하고, 학생별 누락 여부를 표로 정리해. 모든 항목을 확인하면 끝내고, 자료가 없거나 판단이 필요한 경우에만 질문해”처럼 쓸 수 있습니다. 파괴적인 작업이나 사람의 승인이 필요한 단계는 계속 진행하지 말고 멈추도록 따로 적습니다.

AI가 잘못 이해해서 엉뚱한 방향으로 갈 때도 있지만, 완료 기준이 없어서 서로 다른 결과를 내놓는 경우도 있습니다. 기준을 먼저 주면 결과를 읽을 때도 무엇을 확인해야 하는지 분명해집니다.

긴 업무의 완료 기준을 정하는 순서

  • 숙제 피드백 지시문에 대상, 제출물 형식, 완료 조건, 질문할 조건을 한 문장씩 적습니다.
  • 학부모 안내문에는 “초안을 만들고, 사실·날짜·대상 학생을 점검한 뒤, 판단이 필요한 문장만 질문해”처럼 끝나는 순서를 씁니다.
  • 워크북 검수에는 오탈자·페이지 넘침·문항 연결처럼 검사할 항목과, 직접 확인하지 못한 항목의 보고 형식을 함께 넣습니다.
  • 디자인 요청에는 원하지 않는 배경, 제목 강조, 번호 라벨, 글꼴 라벨, 버튼 모양을 구체적으로 적습니다.
  • 오래 걸리는 교재 제작이나 리포트 작성은 TASKS.md에 할 일을 적고, 새로 생긴 항목도 같은 파일에 추가합니다.
  • 차트나 학습 자료의 스크린샷은 다시 타이핑하지 말고 원본 파일을 첨부한 뒤, 확인할 질문을 한 문장으로 씁니다.

자주 묻는 질문

“단계별로 생각해”를 지워도 되나요?

Opus 5.5는 답변 전에 생각하므로 같은 지시를 반복할 필요가 없다는 것이 가이드의 설명입니다. Anthropic이 채팅 제품에서 해 본 테스트에서는 해당 문구를 지웠을 때 답변이 더 빨리 시작됐고 품질 저하는 뚜렷하지 않았습니다.

완료 기준은 어떻게 쓰나요?

“무엇을 만들고, 어떤 상태면 끝내며, 어떤 경우에만 질문할지”를 한 문장에 넣으면 됩니다. 예를 들면 “초안을 만들고 날짜와 대상만 점검한 뒤, 자료가 없는 항목만 따로 물어봐”처럼 업무의 끝과 중단 조건을 함께 씁니다.

AI가 확인하지 못한 것을 왜 따로 적게 하나요?

사람이 마지막으로 판단해야 할 위치를 바로 찾을 수 있기 때문입니다. 가이드도 찾지 못한 내용과 살펴본 범위를 보고하게 하면 결과를 검토하기 쉬워진다고 설명합니다.

긴 작업은 어디에 적어두나요?

Claude Code를 쓴다면 원문 예시처럼 TASKS.md에 체크리스트를 두고 진행하면서 갱신할 수 있습니다. 화면의 긴 대화 기록보다 파일을 보면 끝난 일과 남은 일을 빠르게 확인할 수 있습니다.

출처

이 글은 claude.dev 원문을 학원 운영 관점에서 요약·해설한 글이며 번역이 아닙니다.

제미나이 4 아르곤 정리: 외부 리더보드 성적과 아직 열리지 않은 것들

제미나이 4 아르곤은 구글이 한국 시간 2026년 10월 1일 오전 5시에 공개한 제미나이 4 첫 프런티어 모델로, 지금은 구글 내부와 페어윈드(Fairwind) 참여 기관만 쓸 수 있고 API·한국 일정은 발표되지 않았다.

미국 시간으로는 9월 30일 발표입니다. 발표 글은 구글 딥마인드의 코라이 카바쿨루(Koray Kavukcuoglu)가 썼습니다. 국내 보도자료에는 화려한 표현이 많이 붙었지만, 공식 글을 직접 읽어 보면 확인된 내용과 아직 정해지지 않은 내용이 꽤 분명하게 갈립니다. 이 글은 그 둘을 나눠서 정리합니다.

한눈에 보기

  • 구글 공식 표현은 ‘새 프런티어 모델(new frontier model)’입니다. 소프트웨어 엔지니어링, 법률·금융 같은 기업 지식 업무, 사이버 보안 방어를 주요 용도로 꼽았습니다.
  • 외부 리더보드에서 단독 1위가 확인된 지표는 Vals Index(68.90%)입니다. DeepSWE 77.9%는 구글이 직접 낸 수치이고, CWE-bench 68%는 세 모델이 공동 1위입니다.
  • 지금은 구글 내부와 페어윈드 프로그램에 선정된 기관만 쓸 수 있습니다. API, 제미나이 앱, 한국 출시 일정은 아직 발표되지 않았습니다.
  • 발표된 가격은 100만 토큰당 입력 2달러·출력 10달러(출시 기간)이고, 그 뒤에는 4달러·20달러입니다.

제미나이 4 아르곤은 어떤 모델인가

아르곤은 제미나이 4 세대의 첫 모델이고, 지금까지는 유일한 제미나이 4 모델입니다. 그동안 구글 최상위 모델에 붙던 ‘Pro’라는 이름 대신 새 이름 체계를 쓴 점이 눈에 띕니다. 로이터에 따르면 구글 대변인은 아르곤이 기존 ‘Pro’ 계열보다 크기가 큰 모델이라고 설명했습니다. 9to5Google은 이번 발표가 제미나이 3.5 Pro 계획이 취소된 뒤에 나왔다고 전했습니다. 공개 전 ‘Gemini 4 Pro’라는 이름으로 돌던 Argon 유출·Arena 논란은 이전 글에 정리해 두었습니다. 당시 유출 스펙(출력 25만 6천 토큰)은 공식 발표(출력 100만 토큰)와 다릅니다.

구글은 공식 글에서 아르곤이 실제 소프트웨어 개발, 법률·금융 같은 기업 지식 업무, 사이버 보안 방어처럼 단계가 많은 작업에서 최상위 수준의 성능을 낸다고 설명합니다. 한 번에 내보낼 수 있는 출력 토큰 한도는 이전 6만 4천 개에서 100만 개로 늘었습니다.

표현 하나는 짚고 넘어가겠습니다. 국내 보도자료에는 ‘역대 가장 지능적인 모델’이라는 문구가 있지만, 구글의 영문·한국어 공식 글에는 이런 표현이 없습니다. 공식 글은 ‘새로운 프론티어 모델(new frontier model)’이라고만 부르고, 로이터가 인용한 대변인 발언은 ‘지금까지 가장 성능이 좋은 모델(most performant model yet)’입니다. 어느 쪽이든 회사가 스스로 내린 평가라는 점은 같습니다.

벤치마크 수치, 누가 측정했나

모델 발표에서 가장 많이 인용되는 것이 벤치마크 점수입니다. 다만 같은 ‘1위’라도 회사가 직접 잰 수치인지, 외부 평가 기관이 공개한 수치인지에 따라 무게가 다릅니다. 아래 표는 그 구분을 기준으로 정리한 것입니다.

벤치마크 아르곤 비교 검증 상태
Vals Index 68.90% (1위) Claude Sonnet 5.5 67.04%, Claude Opus 5.5 66.97% 외부 리더보드 게재 (vals.ai, 사이트 표기 9/30 갱신)
CWE-bench v1 68% (공동 1위) Grok 4.7 68%, GPT-6 Astra 68%, Opus 5.5 67% 외부 리더보드 게재 (cwe-bench.com), 3개 모델 동률
DeepSWE v1.1 77.9% Opus 5.5 74.2, GPT-6 Astra 74.1 (구글 표 기준) 구글 자체 수치, 공식 리더보드 미등재
LVBench / AutomationBench 91.7% / 51.3% 구글은 각각 최고 기록·1위라고 발표 구글 발표 기준

Vals Index 1위: 외부 리더보드에 오른 수치

Vals Index는 금융, 코딩, 법률, 세무 과제를 미국 GDP 비중에 따라 가중해 점수를 매기는 지표입니다(사이트 표기 날짜 기준 9월 25일 세무 항목이 추가된 v2.1). vals.ai 공식 페이지에서 아르곤은 68.90%로 41개 모델 중 1위, Claude Sonnet 5.5가 67.04%로 2위, Claude Opus 5.5가 66.97%로 3위입니다. 참고로 vals.ai는 아르곤을 출시 기간 이후 가격인 100만 토큰당 4달러·20달러 기준으로 표시했습니다. vals.ai의 별도 지표 Vals RSI Index에서는 23개 중 4위(30.55%)입니다. 일부 해외 매체가 말한 ‘4위’는 이 지표 기준입니다.

CWE-bench 68%: 단독 1위가 아니라 3개 모델 동률

보안 취약점 관련 과제를 평가하는 CWE-bench v1(Collinear AI가 Artificial Analysis와 함께 공개)에서 아르곤은 자동 채점 기준(programmatic pass@1) 68%를 받았습니다. 그런데 같은 68%에 Grok 4.7과 GPT-6 Astra도 있어 세 모델이 공동 1위입니다. 심사단 채점 기준(judge-panel pass@1)으로 보면 아르곤은 62%로, GPT-6 Astra(66%), Opus 5.5(67%), Grok 4.7(64%)보다 낮습니다. 구글이 공개한 비교표에는 Grok 4.7이 빠져 있다는 점도 함께 알아 두면 좋습니다.

DeepSWE 77.9%: 구글이 직접 측정한 수치

구글은 실전형 소프트웨어 엔지니어링 벤치마크 DeepSWE v1.1에서 77.9%로 최고 기록을 세웠다고 밝혔습니다. 다만 9월 22일(사이트 표기 날짜)에 마지막으로 갱신된 DeepSWE 공식 리더보드에는 아직 아르곤이 올라 있지 않습니다. 지금 시점에서는 구글이 자체 측정해 발표한 수치이고, 외부에서 다시 확인된 기록은 아닙니다. 구글의 평가 방법 문서에도 아르곤의 DeepSWE v1.1 점수는 ‘자체 계산(self computed)’이라고 적혀 있습니다. 같은 문서에 따르면 비교 대상 중 GPT-6 Astra 점수는 공식 공개 리더보드에서, Fable 5.1과 Opus 5.5 점수는 각 회사의 시스템 카드에서 가져왔습니다.

구글 표에서 아르곤이 뒤지는 항목

구글 딥마인드가 공개한 비교표에도 아르곤이 경쟁 모델보다 낮은 항목이 있습니다. FrontierSWE v2에서는 55.0으로 GPT-6 Astra(65.5)보다 낮고, Terminal-Bench 4.0에서는 57.4로 Opus 5.5(66.4)보다 낮습니다. Terminal-Bench Science, PostTrainBench, OSWorld-2.0에서도 다른 모델이 앞섭니다. 여러 지표를 종합하는 Artificial Analysis Intelligence Index에서는 53점으로 Fable 5.1, GPT-6 Astra와 함께 공동 3위라고 연합뉴스가 보도했습니다. 모든 분야에서 앞서는 모델이라기보다, 강점과 약점이 함께 있는 모델로 보는 편이 정확합니다.

구글 안에서는 어떻게 쓰였나

구글은 이미 수천 명의 직원이 업무에 아르곤을 쓰고 있다고 밝혔습니다. 대표 사례는 C/C++ 코드를 Rust로 옮기는 작업입니다. 정규식 라이브러리 re2, 동영상 디코더 libgav1처럼 수만 줄 규모의 코드부터, 80만 줄이 넘는 Fuchsia 운영체제의 Zircon 커널까지 전환을 시도했습니다. 이 결과물은 실제 서비스에 넣기 전에 감사를 받는 중입니다.

메모리 절감 효과도 소개했는데, 수치를 읽을 때 주의가 필요합니다. 구글은 전면 적용되면 300TiB가 넘는 메모리를 확보하고, 전체적으로는 500TiB에서 1PiB를 아낄 것으로 추정합니다. 이미 달성한 성과가 아니라 예상치입니다. 이 밖에 양자 컴퓨팅 최적화 과제에서 기준 대비 40% 개선된 결과를 냈다고 밝혔습니다.

지금 누가, 언제 쓸 수 있나

아르곤은 한 번에 모두에게 열리지 않고 단계적으로 공개됩니다. 지금 쓸 수 있는 곳은 구글 내부 팀과 페어윈드 프로그램 참여 기관, 그리고 일부 신뢰 테스터뿐입니다. 페어윈드는 사이버 보안 방어를 맡은 조직을 위한 프로그램으로, 개인이 아니라 기관 단위로 심사를 거쳐 참여합니다. 대상은 정부·국가 사이버 당국, 핵심 기반시설 운영 기관(의료·통신·에너지·금융 등), 핵심 기술 플랫폼이며, 구글이 신청 기관의 이력을 심사합니다. 구글은 이 신뢰받는 방어 조직과 내부 팀에는 사이버 관련 안전장치를 뺀 버전을 제공한다고 밝혔습니다. 또 미국 정부의 자발적 사전 모델 접근 절차에도 참여하고 있다고 했습니다. 글 맨 위 도식은 2026년 10월 1일(한국시간) 기준 공개 단계를 정리한 것입니다. 3·4단계 구분은 구글 발표 문장을 바탕으로 정리한 것이며, 날짜는 발표되지 않았습니다(출처: 구글 공식 블로그, 구글 딥마인드 페어윈드 프로그램 페이지).

구글이 밝힌 다음 순서는 유료 API 고객과 Google AI Ultra 구독자입니다. 구글은 개발자, 기업, 일반 사용자에게 ‘가능한 한 빨리’ 제공하겠다고 했지만 날짜는 밝히지 않았습니다. 10월 1일(한국시간) 확인 기준으로 아르곤은 Gemini API 문서의 모델 목록과 가격 페이지, Vertex AI 모델 목록에 없고, 제미나이 앱에도 들어 있지 않습니다. 영문·한국어 공식 글 어디에도 한국 출시 일정에 관한 언급은 없습니다.

구글은 광범위하게 공개하기 전에 네 가지 안전장치를 점검한다고 밝혔습니다. 사이버 공격이나 화학·생물·방사능·핵(CBRN) 분야 오용 방지, 프롬프트 인젝션 대응, 모델이 의도에서 벗어나는지 감시하는 정렬 모니터링, 샌드박스 강화입니다.

가격은 얼마로 발표됐나

구글이 발표한 API 가격은 출시 기간 동안 100만 토큰당 입력 2달러, 출력 10달러입니다. 캐시된 입력은 입력 가격에서 95% 할인됩니다. 영문 공식 글의 각주에 따르면 출시 기간이 끝나면 입력 4달러, 출력 20달러로 오릅니다. 출시 기간이 얼마나 이어지는지는 나와 있지 않습니다. 한국어 공식 글에는 출시 가격만 보이고, 이후 가격 각주는 확인되지 않았습니다. 아직 API가 열리지 않았기 때문에 이 가격은 실제 청구 가격이 아니라 발표된 가격입니다.

지금 한국 사용자가 할 수 있는 것

  • 공개 여부는 공식 페이지에서 확인하기. 구글 AI 개발자 문서(ai.google.dev)의 모델 목록과 가격 페이지, 제미나이 앱에 아르곤이 올라오는지 확인하는 것이 가장 확실합니다. ‘국내 출시’ 같은 표현은 공식 발표가 나온 뒤에 믿어도 늦지 않습니다.
  • 보안 조직이라면 페어윈드 페이지 읽어 보기. 기관 단위로 심사하는 프로그램이므로 개인 개발자가 신청하는 경로는 아닙니다. 참여 조건은 구글 딥마인드 페어윈드 프로그램 페이지에서 확인할 수 있습니다.
  • 비용은 4달러·20달러 기준으로 잡아 두기. 출시 가격이 언제까지인지 정해지지 않았으니, 도입을 검토한다면 이후 가격을 기준으로 예산을 짜는 편이 안전합니다.
  • 벤치마크는 외부 리더보드에 올라오는지 지켜보기. DeepSWE 공식 리더보드에 아르곤이 등재되는지, 구글 표에서 약했던 터미널 작업·에이전트 항목이 어떻게 평가되는지 보면 실제 성능을 가늠하는 데 도움이 됩니다.
  • 평가용 과제를 미리 만들어 두기. 내 업무에서 자주 하는 코드 전환, 문서 검토 같은 작업을 몇 가지 정해 두면, 아르곤이 열렸을 때 지금 쓰는 모델과 바로 비교해 볼 수 있습니다.

자주 묻는 질문

Q. 한국에서 지금 제미나이 4 아르곤을 쓸 수 있나요?

A. 10월 1일(한국시간) 기준 쓸 수 없습니다. 지금은 구글 내부 팀과 심사를 거친 페어윈드 프로그램 참여 기관만 쓸 수 있습니다. API와 제미나이 앱에는 아직 없고, 한국 출시 일정도 발표되지 않았습니다.

Q. ‘역대 가장 지능적인 모델’이라는 말이 맞나요?

A. 이 표현은 국내 보도자료의 문구입니다. 구글 공식 글은 아르곤을 ‘새로운 프론티어 모델’이라고 부를 뿐, ‘가장 지능적인’이라는 표현은 쓰지 않았습니다. 외부 리더보드에서 단독 1위가 확인된 지표는 Vals Index이고, CWE-bench는 공동 1위입니다. 구글 표에서도 FrontierSWE v2, Terminal-Bench 4.0 등에서는 다른 모델이 앞섭니다.

Q. 제미나이 4 아르곤이 ‘Gemini 4 Pro’인가요?

A. 아닙니다. 구글 공식 글에 나온 정식 모델명은 ‘제미나이 4 아르곤(Gemini 4 Argon)’이고, 아르곤은 코드네임이 아니라 공식 모델 이름입니다. 구글은 최상위 모델에 ‘Pro’ 대신 이 이름을 붙였습니다. 로이터에 따르면 구글 대변인은 아르곤이 기존 ‘Pro’ 계열보다 큰 모델이라고 설명했습니다.

Q. 가격은 얼마인가요?

A. 발표 기준으로 출시 기간에는 100만 토큰당 입력 2달러·출력 10달러, 이후에는 4달러·20달러입니다. 캐시된 입력은 95% 할인됩니다. 출시 기간이 얼마나 되는지는 공개되지 않았습니다.

Q. DeepSWE 77.9%는 믿을 만한 수치인가요?

A. 구글이 직접 측정해 발표한 수치이고, 구글 평가 방법 문서에도 ‘자체 계산(self computed)’으로 적혀 있습니다. 10월 1일(한국시간) 기준 DeepSWE 공식 리더보드(사이트 표기 9월 22일 갱신)에는 아르곤이 아직 없어, 외부에서 다시 확인된 기록은 아닙니다.

이 글은 구글 공식 블로그(영문·한국어), 구글 딥마인드 모델 페이지, 구글 평가 방법 문서, 그리고 외부 벤치마크 사이트(vals.ai, CWE-bench, DeepSWE 리더보드)를 2026년 10월 1일(한국시간) 기준으로 직접 확인해 정리했습니다. 수치마다 누가 측정했는지 함께 적었고, 출처 링크는 본문과 글 끝에 모아 두었습니다.

출처

구글 시트로 Gemini Spark 커스텀 앱 만들기 (Apps Script·MCP)

Apps Script + clasp로 MCP 서버를 만들어 Spark에 연결하는 초보자 튜토리얼 (Moonlight Ops 제작기)

이 문서는 Moonlight English(TPT 스토어)의 운영 시트 Moonlight_English_Ops를 Gemini Spark에서 대화로 조회할 수 있게 만든 과정을 그대로 정리한 것입니다. 코딩 경험이 거의 없어도 순서대로 따라 하면 자기 시트로 같은 것을 만들 수 있도록 썼습니다. 실제로 만들면서 막혔던 부분과 해결 방법도 모두 넣었습니다.

1. 완성하면 무엇이 달라지나

전에는

  • 시트를 열고, 탭을 찾고, 필터를 걸고, 원하는 행을 눈으로 찾았습니다.
  • 812개 상품이 있는 카탈로그에서 “Easel이 아직 안 붙은 상품”을 찾으려면 직접 필터를 만들어야 했습니다.

이제는

  • Gemini Spark에 한국어로 물어보면 Spark가 시트를 직접 읽고 답합니다.
    • “이번 주 우선순위 알려줘”
    • “Easel 아직 안 붙은 상품 목록 뽑아줘”
    • “시리즈별 상품 수 세어줘”
  • Spark는 24시간 돌아가는 에이전트라서, “매주 월요일 아침에 파이프라인 상태를 요약해서 메일로 보내줘” 같은 예약 작업에도 이 시트를 쓸 수 있습니다.
  • 시트 내용을 복사해서 붙여넣을 필요가 없습니다. 항상 지금 시트에 있는 최신 값을 읽습니다.

한 줄 요약: 내 구글 시트가 Spark가 쓸 수 있는 “도구”가 됐습니다.

2. 먼저 알아둘 개념 4가지

어려운 말이 몇 개 나오는데, 아래 비유만 기억하면 충분합니다.

용어 쉬운 설명
Gemini Spark Google의 AI 에이전트입니다. 대화뿐 아니라 여러 앱을 써서 일을 대신 처리하고, 예약 작업도 합니다.
MCP (Model Context Protocol) AI와 외부 도구를 잇는 “표준 콘센트”입니다. 이 규격에 맞춰 만들면 Spark, Claude, Cursor 같은 AI가 그 도구를 꽂아 쓸 수 있습니다.
Custom apps (맞춤 앱) Spark의 설정 메뉴입니다. MCP 서버 주소(URL)를 넣으면 Spark가 그 도구를 쓸 수 있게 됩니다.
Google Apps Script 구글이 무료로 제공하는 작은 서버입니다. 코드를 올리면 인터넷 주소(웹앱 URL)가 생기고, 내 구글 시트도 읽을 수 있습니다.
clasp 내 컴퓨터에 있는 코드 파일을 Apps Script로 올려 주는 명령어 도구입니다. 브라우저에서 코드를 복사해 붙여넣는 수고를 줄여 줍니다.

3. 전체 구조 한눈에 보기

 나 (한국어로 질문)
   │
   ▼
 Gemini Spark ──── "@Moonlight Ops 이번 주 우선순위 알려줘"
   │  (MCP 규격으로 요청: tools/call ops_overview)
   ▼
 Apps Script 웹앱  https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec?accessKey=YOUR_ACCESS_KEY
   │  1) 비밀키가 맞는지 확인
   │  2) 시트를 "읽기 전용"으로 읽음
   ▼
 구글 시트 Moonlight_English_Ops (00_KPI, 01_Catalog, 05_Pipeline ...)

Spark가 쓸 수 있는 도구는 4개입니다.

도구 이름 하는 일 예시 질문
ops_overview 탭 목록, 탭별 행 수와 컬럼, 00_KPI 요약 “시트 전체 현황 알려줘”
ops_read_tab 한 탭을 행 단위로 읽기 (페이지 나눠 읽기 가능) “05_Pipeline 처음 20행 보여줘”
ops_search 글자 검색, 또는 “컬럼 = 값” 조건 검색 “Junie 들어간 행 전부 찾아줘”
ops_count_by 컬럼 값별로 개수 세기 “status별 상품 수 세어줘”

4. 준비물 체크리스트

  • ☐ 개인 구글 계정 (회사/학교 계정은 Spark 맞춤 앱이 안 됩니다)
  • ☐ Gemini Spark 사용 권한 (Google AI Pro 또는 Ultra 구독)
  • ☐ Gemini 설정에서 Keep Activity(활동 기록 유지) 켜기 (꺼져 있으면 맞춤 앱 연결이 안 됩니다)
  • ☐ 연결할 구글 시트 1개
  • ☐ Windows PC (이 튜토리얼은 PowerShell 기준입니다. Mac도 명령어만 조금 다르고 흐름은 같습니다)

참고: Google 안내 기준으로 Spark 맞춤 앱은 개인 계정, Keep Activity 켜짐, 영어 UI 등의 조건이 있습니다. 메뉴가 안 보이면 이 조건부터 확인하세요.

5. 1단계: 컴퓨터 준비 (Node.js, clasp)

5-1. Node.js 설치

  1. https://nodejs.org 에서 LTS 버전을 받아 설치합니다. (다음, 다음, 설치만 누르면 됩니다)
  2. PowerShell을 새로 열고 확인합니다.
node -v
npm -v

버전 숫자가 나오면 성공입니다.

5-2. clasp 설치

npm i -g @google/clasp
clasp --version

5-3. Apps Script API 켜기 (중요, 자주 빠뜨림)

  1. 브라우저에서 https://script.google.com/home/usersettings 에 들어갑니다.
  2. Google Apps Script API를 On으로 바꿉니다.

이걸 안 켜면 clasp가 프로젝트를 만들거나 올리지 못합니다.

5-4. clasp 로그인

clasp login

브라우저가 열리면 시트를 가진 구글 계정으로 로그인하고 허용을 누릅니다. “Logged in” 같은 메시지가 나오면 끝입니다.

6. 2단계: Apps Script 프로젝트 만들기

작업할 폴더를 만들고, 그 안에서 프로젝트를 만듭니다.

mkdir $HOME\moonlight-ops-mcp
cd $HOME\moonlight-ops-mcp
clasp create --type standalone --title "Moonlight Ops MCP"

성공하면 이렇게 나옵니다.

Created new script: https://script.google.com/d/YOUR_SCRIPT_ID/edit
└─ appsscript.json

폴더 안에 .clasp.json(어느 프로젝트와 연결됐는지 적힌 파일)과 appsscript.json(설정 파일)이 생깁니다.

7. 3단계: 코드와 설정 파일 넣기

폴더에 파일이 2개 있어야 합니다.

파일 역할
Code.gs MCP 서버 코드 (Spark의 요청을 받아 시트를 읽고 답함)
appsscript.json 설정 파일 (웹앱으로 공개, 읽기 전용 권한, Sheets API 사용)

7-1. Code.gs

부록의 전체 코드를 Code.gs라는 이름으로 저장합니다. 메모장이나 VS Code에 붙여넣고 폴더에 저장하면 됩니다.

꼭 바꿔야 하는 곳은 딱 1줄입니다.

const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';

시트 주소가 https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID/edit라면, /d/와 /edit 사이의 YOUR_SPREADSHEET_ID 부분이 ID입니다.

내 시트에 맞게 바꾸면 좋은 곳: TOOLS 안의 description 문장들. 여기에 “이 시트가 무엇이고 탭이 어떤 의미인지”를 적어 두면 Spark가 도구를 더 똑똑하게 고릅니다.

7-2. appsscript.json

주의: 이 파일은 반드시 “BOM 없는 UTF-8″로 저장해야 합니다. Windows PowerShell 5.1의 Set-Content -Encoding utf8은 파일 맨 앞에 BOM이라는 보이지 않는 3바이트를 붙이는데, clasp가 이걸 읽지 못하고 Invalid manifest file 오류를 냅니다. (저희도 여기서 한 번 막혔습니다.)

아래 명령을 폴더 안에서 그대로 실행하면 안전하게 저장됩니다.

$m = @'
{
  "timeZone": "Asia/Seoul",
  "dependencies": {
    "enabledAdvancedServices": [
      { "userSymbol": "Sheets", "version": "v4", "serviceId": "sheets" }
    ]
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "ANYONE_ANONYMOUS"
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets.readonly"
  ]
}
'@
[IO.File]::WriteAllText("$PWD\appsscript.json", $m, (New-Object Text.UTF8Encoding $false))

각 줄의 의미:

설정 의미
enabledAdvancedServices: Sheets 시트를 Sheets API로 읽습니다. 읽기 전용 권한만으로 읽기 위해 필요합니다. (15장 문제 2 참고)
webapp.executeAs: USER_DEPLOYING 웹앱이 내 권한으로 시트를 읽습니다.
webapp.access: ANYONE_ANONYMOUS Spark가 로그인 없이 주소로 접근할 수 있게 합니다. 대신 코드가 비밀 접근 키로 막습니다.
oauthScopes: spreadsheets.readonly 이 프로그램은 시트를 읽기만 할 수 있습니다. 수정/삭제 불가.

8. 4단계: 올리기(push)와 배포(deploy)

clasp push -f

이렇게 나오면 성공입니다.

Pushed 2 files.
└─ appsscript.json
└─ Code.gs

Pushed 2 files가 안 나오고 오류가 나면 절대 다음 단계로 넘어가지 마세요. (15장 참고)

이어서 배포합니다.

clasp deploy --description "v1"
Deployed YOUR_DEPLOYMENT_ID @2

YOUR_DEPLOYMENT_ID 자리에 나오는 긴 문자열(AKfycb…로 시작)이 배포 ID입니다. 메모해 두세요. 웹앱 주소는 이렇게 됩니다.

https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec

팁: 나중에 코드를 고치고 다시 배포할 때는 clasp deploy -i YOUR_DEPLOYMENT_ID -d "v2"처럼 -i를 붙이세요. 그래야 주소가 바뀌지 않아서 Spark 연결을 다시 할 필요가 없습니다.

9. 5단계: 접근 키 저장하기

웹앱은 누구나 주소로 접근할 수 있게 열려 있으므로, 비밀 키를 아는 요청만 받도록 합니다.

9-1. 랜덤 키 만들기

PowerShell에서 실행하면 안전한 랜덤 문자열이 나옵니다.

$b = New-Object byte[] 24
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b)
[Convert]::ToBase64String($b).Replace('+','-').Replace('/','_').TrimEnd('=')

나온 문자열을 복사해 둡니다. 비밀번호처럼 다루세요. 채팅, 스크린샷, 공개 저장소에 올리면 안 됩니다.

9-2. Script Properties에 저장

  1. https://script.google.com 에서 프로젝트를 엽니다.
  2. 왼쪽 톱니바퀴 **Project Settings(프로젝트 설정)**를 누릅니다.
  3. 맨 아래 Script Properties 에서 Add script property를 누릅니다.
  4. Property: ACCESS_KEY, Value: 방금 만든 키
  5. Save script properties를 누릅니다.

코드는 이 값을 읽어서, 요청 주소의 ?accessKey= 값과 같을 때만 응답합니다. 키를 코드에 직접 쓰지 않아서, 나중에 코드를 공유해도 키가 새지 않습니다.

10. 6단계: 권한 승인하기

웹앱이 “내 권한으로” 시트를 읽으려면, 처음 한 번 내가 직접 허락해야 합니다. 이걸 안 하면 웹앱 주소가 Authorization needed 페이지만 돌려줍니다.

  1. 프로젝트 편집기(Editor)에서 Code.gs를 엽니다.
  2. 위쪽 함수 선택 칸에서 **testLocal**을 고릅니다.
  3. **Run(실행)**을 누릅니다.
  4. “Authorization required” 창이 뜨면 Review permissions를 누릅니다.
  5. 계정을 고르면 “Google hasn’t verified this app” 경고가 나옵니다.
    • 놀라지 마세요. 개발자가 나 자신이라서 나오는 정상 경고입니다.
    • Advanced(고급) → Go to Moonlight Ops MCP (unsafe) 를 누릅니다.
  6. 요청 권한이 “See all your Google Sheets spreadsheets”(시트 보기) 하나뿐인지 확인하고 **Continue(계속)**를 누릅니다.
  7. 아래 Execution log에 도구 목록과 시트 요약이 찍히면 성공입니다.

11. 7단계: 제대로 동작하는지 확인하기

Spark에 붙이기 전에 직접 호출해 보면 문제를 빨리 찾을 수 있습니다. PowerShell에서 실행합니다. (YOUR_DEPLOYMENT_ID, YOUR_ACCESS_KEY를 바꾸세요)

$url = "https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec?accessKey=YOUR_ACCESS_KEY"
$body = '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Invoke-RestMethod -Uri $url -Method Post -ContentType "application/json" -Body $body | ConvertTo-Json -Depth 6

ops_overview, ops_read_tab, ops_search, ops_count_by 4개가 보이면 성공입니다.

시트를 실제로 읽는지도 확인합니다.

$body = '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ops_overview","arguments":{}}}'
Invoke-RestMethod -Uri $url -Method Post -ContentType "application/json" -Body $body | ConvertTo-Json -Depth 6

틀린 키로도 한 번 해 보세요. Unauthorized 오류가 나와야 정상입니다.

저희가 실제로 확인한 결과:

  • initialize, tools/list 정상 응답
  • ops_overview가 실제 시트 값 반환 (01_Catalog 812행, 03_Listing_QA 774행, 02_Series 73행 등)
  • 틀린 키는 Unauthorized: missing or wrong accessKey로 거부

12. 8단계: Gemini Spark에 연결하기

  1. https://gemini.google.com/spark/apps 로 갑니다. (또는 Gemini 왼쪽 메뉴 Spark → Connected Apps)
  2. Custom apps 섹션에서 Add a custom app을 누릅니다.
  3. MCP Server URL 칸에 키까지 포함한 주소를 넣습니다.
    https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec?accessKey=YOUR_ACCESS_KEY
  4. Next → 안내문을 읽고 “I understand and accept…” 체크 → Connect
  5. “Save your custom app” 창이 뜨고 Available actions에 도구 4개가 보이면 성공입니다.
  6. 앱 이름을 확인(예: Moonlight Ops)하고 Connect를 누릅니다.

이제 Custom apps 목록에 Moonlight Ops가 켜진 상태로 보입니다.

왜 이 방식이 잘 되나: Spark 맞춤 앱은 로그인(OAuth)이 필요한 서버와 연결할 때 실패 사례가 많이 보고돼 있습니다. 이 방식은 로그인 대신 주소에 든 키로 인증하므로, Spark 입장에서는 로그인 절차가 없는 단순한 서버라 안정적으로 연결됩니다.

13. 9단계: 실제로 써보기

Spark 대화창에서 @Moonlight Ops를 붙여 물어보면 됩니다. 처음 도구를 쓸 때 Spark가 “이 앱을 사용해도 될까요?”라고 물으면 허용합니다.

현황 파악

  • @Moonlight Ops 시트 전체 현황이랑 이번 주 우선순위 알려줘

찾기

  • @Moonlight Ops 01_Catalog에서 easel_status가 비어있는 상품 목록 보여줘
  • @Moonlight Ops Junie 들어간 행 전부 찾아서 탭별로 정리해줘
  • @Moonlight Ops 03_Listing_QA에서 listing_score 낮은 상품 10개

집계

  • @Moonlight Ops 01_Catalog를 series별로 개수 세줘
  • @Moonlight Ops 05_Pipeline stage별 개수

다른 앱과 섞어 쓰기 (Spark의 진짜 힘)

  • @Moonlight Ops 파이프라인에서 live 된 것들 뽑아서 구글 문서로 주간 리포트 만들어줘
  • 매주 월요일 오전 9시에 @Moonlight Ops 로 00_KPI 요약해서 내 Gmail로 보내줘 (Spark 예약 작업)

14. 이걸로 무엇을 할 수 있게 되었나

14-1. 시트를 “대화로” 조회

필터나 수식을 몰라도 됩니다. “Easel 안 붙은 Digital 상품”처럼 말로 조건을 주면 Spark가 알맞은 도구와 조건을 골라 시트를 읽습니다.

14-2. 항상 최신 데이터

시트를 복사해서 AI에 붙여넣는 방식은 금방 옛날 자료가 됩니다. 이 방식은 물어보는 순간의 시트를 읽습니다. 시트를 고치면 바로 답이 바뀝니다.

14-3. 예약 작업과 자동화

Spark는 컴퓨터를 꺼도 클라우드에서 돌아갑니다. “매주”, “매일 아침” 같은 일정으로 시트 요약, 리포트 작성, 메일 발송을 맡길 수 있습니다.

14-4. 여러 앱을 한 번에 연결

Spark에 연결된 다른 앱과 조합할 수 있습니다.

조합 할 수 있는 일
Moonlight Ops + Google Docs/Gmail 시트 기반 주간 리포트 작성, 메일 발송
Moonlight Ops + Exa 카탈로그의 시리즈와 경쟁 상품을 웹에서 비교 조사
Moonlight Ops + Canva 우선순위 상품의 홍보 이미지 초안
Moonlight Ops + DeepWiki / GitHub 워크북 제작 도구(harness) 코드와 시트 상태를 함께 확인

14-5. 안전한 구조

  • 읽기 전용이라 AI가 실수해도 시트가 망가지지 않습니다.
  • 비밀 키가 없으면 아무도 못 씁니다.
  • Google 서버에서 무료로 돌아가서 따로 서버 비용이 없습니다.

14-6. 같은 틀로 계속 늘릴 수 있음

이번에 만든 코드는 “도구 목록(TOOLS) + 도구 실행 함수” 구조라서, 도구를 하나씩 더 붙이기 쉽습니다. 다른 시트(예: 공고 스캔 결과, 레포 대시보드)도 같은 방법으로 Spark 전용 앱으로 만들 수 있습니다. (17장 참고)

15. 실제로 겪은 문제와 해결법

직접 만들면서 부딪힌 문제 3가지입니다. 초보자가 가장 많이 막히는 곳이기도 합니다.

문제 1. clasp push에서 Invalid manifest file "appsscript.json"

  • 증상: push가 실패했는데, 모르고 deploy까지 진행했습니다. deploy 자체는 “Deployed …”라고 성공처럼 나옵니다.
  • 원인: PowerShell 5.1의 Set-Content -Encoding utf8이 파일 앞에 BOM을 붙였고, clasp가 이를 잘못된 JSON으로 봤습니다.
  • 해결: 7-2의 [IO.File]::WriteAllText(..., UTF8Encoding $false) 방식으로 BOM 없이 저장한 뒤 다시 push했습니다.
  • 교훈: push 결과에 Pushed 2 files가 나오는지 반드시 확인하세요.

문제 2. 웹앱 주소가 404를 반환

  • 증상: 배포 ID로 주소를 열면 404가 나왔습니다. 배포 관리 화면을 보니 배포가 “Library(라이브러리)”로만 만들어져 있었습니다.
  • 원인: 문제 1 때문에 코드와 웹앱 설정이 서버에 올라가지 않은 빈 프로젝트를 배포한 것이었습니다. 웹앱 설정(webapp)이 없는 프로젝트를 배포하면 웹앱 주소가 생기지 않습니다.
  • 해결: push를 제대로 한 뒤 clasp deploy -i YOUR_DEPLOYMENT_ID로 같은 배포를 업데이트했습니다. 주소는 그대로 두고 웹앱으로 바뀌었습니다.
  • 확인법: 편집기 왼쪽 파일 목록에 Code.gs가 있는지, appsscript.json 안에 webapp이 있는지 보세요.

문제 3. Specified permissions are not sufficient to call SpreadsheetApp.openById

  • 증상: 도구 목록은 잘 나오는데, 시트를 읽는 도구만 오류가 났습니다.
  • 원인: Apps Script의 기본 시트 도구인 SpreadsheetApp.openById는 시트 쓰기 권한 전체(spreadsheets)를 요구합니다. 저희는 안전을 위해 읽기 전용(spreadsheets.readonly)만 줬기 때문에 막혔습니다.
  • 선택지:
    1. 권한을 전체(spreadsheets)로 넓힌다: 쉽지만, 인터넷에 열린 웹앱이 시트 수정 권한까지 갖게 됩니다.
    2. Sheets API 고급 서비스로 읽는다 (선택한 방법): 읽기 전용 권한으로도 동작합니다.
  • 해결: 코드를 Sheets.Spreadsheets.get, Sheets.Spreadsheets.Values.batchGet으로 바꾸고, 설정 파일에 enabledAdvancedServices로 Sheets를 추가했습니다.

그 밖에 알아두면 좋은 것

증상 원인과 해결
주소를 열면 Authorization needed 10장 권한 승인을 아직 안 함. testLocal을 한 번 실행해 승인
Spark에서 “We had trouble connecting” 일시적 오류일 수 있음. 11장 방법으로 서버가 응답하는지 확인 후 다시 시도 (저희도 다른 앱에서 두 번째 시도에 성공한 적이 있음)
Spark에 Custom apps 메뉴가 없음 개인 계정인지, Spark 권한이 있는지, Keep Activity가 켜져 있는지 확인
코드를 고쳤는데 Spark 답이 그대로 push만 하고 deploy를 안 함. clasp deploy -i YOUR_DEPLOYMENT_ID 실행
도구가 늘었는데 Spark가 모름 Connected Apps → 앱의 More details에서 동기화 시각 확인. 필요하면 앱을 다시 연결

16. 보안에 대해 꼭 알아둘 것

  • 접근 키 = 비밀번호입니다. 주소 전체(...?accessKey=...)를 공개된 곳에 붙이지 마세요.
  • 키가 새면: Script Properties에서 ACCESS_KEY를 새 값으로 바꾸고, Spark에서 앱을 지운 뒤 새 주소로 다시 연결하면 됩니다. 옛 키는 즉시 쓸모없어집니다.
  • 읽기 전용 권한만 줬기 때문에, 키가 새도 시트를 고치거나 지울 수는 없습니다. 다만 시트 내용은 읽힐 수 있으니 키 관리가 중요합니다.
  • Google은 Spark 맞춤 앱으로 연결한 외부 서버를 검토하거나 보호하지 않는다고 안내합니다. 이 경우 서버가 내 소유이고 코드도 직접 확인할 수 있어서 그 걱정이 가장 적은 형태입니다.
  • 나중에 “쓰기” 도구(행 추가, 상태 변경 등)를 만들고 싶다면, 권한이 넓어지는 만큼 별도 프로젝트로 분리하고 키도 따로 두는 것을 권합니다.

17. 나중에 고치고 늘리는 법

코드 수정 → 반영

cd $HOME\moonlight-ops-mcp
# Code.gs 수정 후
clasp push -f
clasp deploy -i YOUR_DEPLOYMENT_ID -d "v1.2 설명"

-i를 쓰면 주소가 그대로라서 Spark는 다시 연결할 필요가 없습니다.

도구 하나 추가하는 방법 (예: 파이프라인 마감 임박 목록)

  1. TOOLS 배열에 도구 설명을 추가합니다. (이름, 설명, 입력값 형식)
  2. callTool_ 함수의 switch에 case '새도구이름': data = 새함수_(args); break;를 추가합니다.
  3. 새함수_를 만듭니다. table_('05_Pipeline')로 행을 받아 원하는 조건으로 거르면 됩니다.
  4. push, deploy 후 Spark의 앱 상세에서 도구가 늘었는지 확인합니다.

다른 시트로 새 앱 만들기

이 폴더를 통째로 복사해 SPREADSHEET_ID와 도구 설명만 바꾸고, 2단계부터 다시 하면 됩니다. 앱마다 키는 새로 만드세요.

18. 자주 묻는 질문

Q. 돈이 드나요? Apps Script는 무료이고 Google 계정의 일일 사용량 한도 안에서 동작합니다. 개인 업무용 조회로는 넉넉합니다. Spark 자체는 AI Pro/Ultra 구독이 필요합니다.

Q. clasp 없이 브라우저만으로도 되나요? 됩니다. script.google.com에서 새 프로젝트를 만들고, 편집기에 코드를 붙여넣고, Project Settings에서 “Show appsscript.json”을 켜서 설정을 붙여넣은 뒤, Deploy → New deployment → Web app(Execute as: Me, Who has access: Anyone)으로 배포하면 됩니다. clasp는 이 과정을 명령어로 빠르게, 실수 없이 반복하게 해 줍니다.

Q. Claude나 Cursor에서도 쓸 수 있나요? MCP 표준을 따르므로 같은 주소를 원격 MCP 서버로 등록하는 도구라면 대부분 쓸 수 있습니다. 다만 클라이언트마다 연결 방식이 조금씩 달라서 직접 확인이 필요합니다.

Q. 시트가 아주 크면 느리지 않나요? 한 번 호출할 때 시트 전체를 한 번에 읽고(batchGet), 결과는 기본 50행(최대 200행)으로 잘라 보냅니다. 셀 하나가 500자를 넘으면 잘라서 보냅니다. 수천 행 규모까지는 문제없이 쓸 수 있습니다.

Q. 컴퓨터를 꺼도 되나요? 됩니다. 서버는 Google에서, Spark는 클라우드에서 돌아갑니다. 내 컴퓨터는 코드를 고칠 때만 필요합니다.

19. 부록: 전체 코드

Code.gs

SPREADSHEET_ID를 내 시트 ID로 바꾸세요. TOOLS의 설명 문장은 내 시트 내용에 맞게 고치면 Spark가 더 정확하게 도구를 씁니다.

/**
 * Moonlight Ops MCP server (read-only) for Gemini Spark custom apps.
 *
 * Deploy: Deploy > New deployment > Web app
 *   Execute as: Me   |   Who has access: Anyone
 * Script property ACCESS_KEY must be set (Project Settings > Script properties).
 * Spark URL: https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec?accessKey=YOUR_ACCESS_KEY
 */
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID'; // <- 내 시트 ID로 바꾸기
const SERVER_INFO = { name: 'moonlight-ops', version: '1.0.0' };
const SUPPORTED_PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05'];
const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 200;
const MAX_CELL_CHARS = 500;

const FILTERS_SCHEMA = {
  type: 'object',
  description: 'Exact, case-insensitive column matches, e.g. {"status":"active","easel_status":""}. Empty string means the cell is blank.',
  additionalProperties: { type: 'string' }
};
const COLUMNS_SCHEMA = {
  type: 'array',
  items: { type: 'string' },
  description: 'Optional. Only return these columns.'
};

const TOOLS = [
  {
    name: 'ops_overview',
    description: 'Overview of the Moonlight English (Teachers Pay Teachers store) ops spreadsheet Moonlight_English_Ops: every tab with data row count and column headers, plus the 00_KPI snapshot and this-week priorities. Call this first.',
    inputSchema: { type: 'object', properties: {} },
    annotations: { title: 'Ops overview', readOnlyHint: true }
  },
  {
    name: 'ops_read_tab',
    description: 'Read rows from one tab as objects keyed by header. Tabs: 00_KPI, 01_Catalog (master SKU list), 02_Series, 03_Listing_QA, 04_Content_QA, 05_Pipeline, 06_Easel, 07_Marketing, 08_Hygiene. Partial names like "catalog" work.',
    inputSchema: {
      type: 'object',
      properties: {
        tab: { type: 'string', description: 'Tab name or partial name, e.g. "01_Catalog" or "pipeline".' },
        offset: { type: 'integer', minimum: 0, description: 'Data rows to skip. Default 0.' },
        limit: { type: 'integer', minimum: 1, maximum: MAX_LIMIT, description: 'Max rows to return. Default 50.' },
        columns: COLUMNS_SCHEMA
      },
      required: ['tab']
    },
    annotations: { title: 'Read tab', readOnlyHint: true }
  },
  {
    name: 'ops_search',
    description: 'Find rows by free text and/or exact column filters, in one tab or across all tabs. Examples: query "Junie"; tab "01_Catalog" with filters {"easel_status":""}; tab "05_Pipeline" with filters {"stage":"live"}.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Case-insensitive substring matched against every cell.' },
        tab: { type: 'string', description: 'Optional tab name or partial name. Omit to search all tabs.' },
        filters: FILTERS_SCHEMA,
        limit: { type: 'integer', minimum: 1, maximum: MAX_LIMIT, description: 'Max rows to return. Default 50.' },
        columns: COLUMNS_SCHEMA
      }
    },
    annotations: { title: 'Search rows', readOnlyHint: true }
  },
  {
    name: 'ops_count_by',
    description: 'Count rows in a tab grouped by one column. Examples: 01_Catalog by status, line, series, easel_status, harness_status, edition; 05_Pipeline by stage. Optional filters.',
    inputSchema: {
      type: 'object',
      properties: {
        tab: { type: 'string', description: 'Tab name or partial name. Default 01_Catalog.' },
        column: { type: 'string', description: 'Column header to group by.' },
        filters: FILTERS_SCHEMA
      },
      required: ['column']
    },
    annotations: { title: 'Count by column', readOnlyHint: true }
  }
];

// ---------- HTTP entry points ----------

function doGet(e) {
  if (!isAuthorized_(e)) return json_({ error: 'unauthorized' });
  return json_({ ok: true, server: SERVER_INFO, transport: 'POST JSON-RPC 2.0 (MCP streamable HTTP, JSON responses)' });
}

function doPost(e) {
  if (!isAuthorized_(e)) return json_(rpcError_(null, -32001, 'Unauthorized: missing or wrong accessKey'));
  let msg;
  try {
    msg = JSON.parse((e.postData && e.postData.contents) || '');
  } catch (err) {
    return json_(rpcError_(null, -32700, 'Parse error'));
  }
  if (Array.isArray(msg)) {
    const out = msg.map(handle_).filter(function (r) { return r; });
    return out.length ? json_(out) : empty_();
  }
  const res = handle_(msg);
  return res ? json_(res) : empty_();
}

// ---------- JSON-RPC / MCP ----------

function handle_(msg) {
  if (!msg || typeof msg !== 'object') return rpcError_(null, -32600, 'Invalid Request');
  const id = msg.id;
  const isNotification = id === undefined || id === null;
  const method = msg.method;
  if (!method) return null; // a client response; nothing to do
  try {
    switch (method) {
      case 'initialize': {
        const requested = msg.params && msg.params.protocolVersion;
        const version = SUPPORTED_PROTOCOLS.indexOf(requested) >= 0 ? requested : SUPPORTED_PROTOCOLS[0];
        return rpcResult_(id, {
          protocolVersion: version,
          capabilities: { tools: { listChanged: false } },
          serverInfo: SERVER_INFO,
          instructions: 'Read-only access to Moonlight_English_Ops, the ops sheet for the Moonlight English TPT store. Start with ops_overview, then use ops_search / ops_read_tab / ops_count_by.'
        });
      }
      case 'ping':
        return rpcResult_(id, {});
      case 'tools/list':
        return rpcResult_(id, { tools: TOOLS });
      case 'tools/call':
        return rpcResult_(id, callTool_(msg.params || {}));
      case 'resources/list':
        return rpcResult_(id, { resources: [] });
      case 'resources/templates/list':
        return rpcResult_(id, { resourceTemplates: [] });
      case 'prompts/list':
        return rpcResult_(id, { prompts: [] });
      default:
        if (isNotification) return null; // notifications/initialized etc.
        return rpcError_(id, -32601, 'Method not found: ' + method);
    }
  } catch (err) {
    return isNotification ? null : rpcError_(id, -32603, String((err && err.message) || err));
  }
}

function callTool_(params) {
  const name = params.name;
  const args = params.arguments || {};
  let data;
  try {
    switch (name) {
      case 'ops_overview': data = overview_(); break;
      case 'ops_read_tab': data = readTab_(args); break;
      case 'ops_search': data = search_(args); break;
      case 'ops_count_by': data = countBy_(args); break;
      default:
        return { content: [{ type: 'text', text: 'Unknown tool: ' + name }], isError: true };
    }
  } catch (err) {
    return { content: [{ type: 'text', text: 'Error: ' + ((err && err.message) || err) }], isError: true };
  }
  return { content: [{ type: 'text', text: JSON.stringify(data) }] };
}

// ---------- Tools ----------

function overview_() {
  const meta = meta_();
  const all = allValues_();
  const tabs = sheetNames_().map(function (name) {
    const values = all[name] || [];
    const header = (values[0] || []).filter(String);
    const dataRows = values.slice(1).filter(function (r) { return r.some(String); }).length;
    return { name: name, data_rows: dataRows, header: header };
  });
  const kpiName = sheetNames_().filter(function (n) { return /kpi/i.test(n); })[0];
  const kpi = kpiName ? rawRows_(kpiName, 60) : [];
  return { spreadsheet: meta.properties.title, url: meta.spreadsheetUrl, tabs: tabs, kpi_rows: kpi };
}

function readTab_(a) {
  const sheet = resolveSheet_(a.tab);
  if (/kpi/i.test(sheet)) {
    return { tab: sheet, note: 'KPI tab has no header row; raw rows returned.', rows: rawRows_(sheet, clampLimit_(a.limit)) };
  }
  const t = table_(sheet);
  const offset = Math.max(0, parseInt(a.offset, 10) || 0);
  const limit = clampLimit_(a.limit);
  const rows = t.rows.slice(offset, offset + limit).map(function (r) { return pick_(r, a.columns); });
  return { tab: sheet, header: t.header, total_rows: t.rows.length, offset: offset, returned: rows.length, rows: rows };
}

function search_(a) {
  const q = a.query ? String(a.query).toLowerCase().trim() : '';
  const filters = a.filters || {};
  if (!q && !Object.keys(filters).length) throw new Error('Give a query and/or filters.');
  const sheets = a.tab ? [resolveSheet_(a.tab)] : sheetNames_().filter(function (n) { return !/kpi/i.test(n); });
  const limit = clampLimit_(a.limit);
  const out = [];
  let total = 0;
  sheets.forEach(function (sheet) {
    const t = table_(sheet);
    t.rows.forEach(function (r) {
      if (!matches_(r, q, filters)) return;
      total++;
      if (out.length < limit) out.push(Object.assign({ _tab: sheet }, pick_(r, a.columns)));
    });
  });
  return { query: a.query || null, filters: filters, total_matches: total, returned: out.length, rows: out };
}

function countBy_(a) {
  const sheet = resolveSheet_(a.tab || '01_Catalog');
  const t = table_(sheet);
  const want = String(a.column || '').toLowerCase().trim();
  const col = t.header.filter(function (h) { return h.toLowerCase() === want; })[0];
  if (!col) throw new Error('Unknown column "' + a.column + '". Columns: ' + t.header.join(', '));
  const counts = {};
  t.rows.forEach(function (r) {
    if (!matches_(r, '', a.filters || {})) return;
    const v = r[col] === undefined ? '(blank)' : String(r[col]);
    counts[v] = (counts[v] || 0) + 1;
  });
  const groups = Object.keys(counts)
    .map(function (k) { return { value: k, count: counts[k] }; })
    .sort(function (x, y) { return y.count - x.count; });
  const total = groups.reduce(function (s, g) { return s + g.count; }, 0);
  return { tab: sheet, column: col, total: total, groups: groups };
}

// ---------- Sheet helpers ----------

// Uses the Sheets advanced service (read-only scope). Sheets are addressed by tab name.
let META_, ALL_;
function meta_() {
  if (!META_) {
    META_ = Sheets.Spreadsheets.get(SPREADSHEET_ID, { fields: 'properties.title,spreadsheetUrl,sheets.properties.title' });
  }
  return META_;
}

function sheetNames_() {
  return (meta_().sheets || []).map(function (s) { return s.properties.title; });
}

function a1_(name) {
  return "'" + String(name).replace(/'/g, "''") + "'";
}

function allValues_() {
  if (!ALL_) {
    const names = sheetNames_();
    const res = Sheets.Spreadsheets.Values.batchGet(SPREADSHEET_ID, { ranges: names.map(a1_), valueRenderOption: 'FORMATTED_VALUE' });
    ALL_ = {};
    (res.valueRanges || []).forEach(function (vr, i) { ALL_[names[i]] = vr.values || []; });
  }
  return ALL_;
}

function values_(name) {
  const rows = allValues_()[name] || [];
  let width = 0;
  rows.forEach(function (r) { width = Math.max(width, r.length); });
  return rows.map(function (r) {
    const out = [];
    for (let i = 0; i < width; i++) out.push(r[i] === undefined || r[i] === null ? '' : String(r[i]));
    return out;
  });
}

function resolveSheet_(tab) {
  if (!tab) throw new Error('tab is required.');
  const names = sheetNames_();
  const q = String(tab).toLowerCase().trim();
  const hit =
    names.filter(function (n) { return n.toLowerCase() === q; })[0] ||
    names.filter(function (n) { return n.toLowerCase().indexOf(q) >= 0; })[0];
  if (!hit) throw new Error('No tab matches "' + tab + '". Tabs: ' + names.join(', '));
  return hit;
}

function table_(sheet) {
  const values = values_(sheet);
  if (!values.length) return { header: [], rows: [] };
  const header = values[0].map(function (h, i) { return String(h).trim() || 'col_' + (i + 1); });
  const rows = [];
  for (let r = 1; r < values.length; r++) {
    const row = values[r];
    if (row.every(function (v) { return v === ''; })) continue;
    const obj = { _row: r + 1 };
    header.forEach(function (h, i) {
      if (row[i] !== '') obj[h] = clip_(row[i]);
    });
    rows.push(obj);
  }
  return { header: header, rows: rows };
}

function rawRows_(sheet, limit) {
  return values_(sheet)
    .filter(function (r) { return r.some(String); })
    .slice(0, limit)
    .map(function (r) {
      let end = r.length;
      while (end > 0 && r[end - 1] === '') end--;
      return r.slice(0, end).map(clip_);
    });
}

function matches_(row, q, filters) {
  const keys = Object.keys(row);
  for (const k in filters) {
    const want = String(filters[k]).toLowerCase().trim();
    const key = keys.filter(function (h) { return h.toLowerCase() === String(k).toLowerCase().trim(); })[0];
    const have = key ? String(row[key]).toLowerCase().trim() : '';
    if (have !== want) return false;
  }
  if (q) {
    return keys.some(function (k) { return k !== '_row' && String(row[k]).toLowerCase().indexOf(q) >= 0; });
  }
  return true;
}

function pick_(row, columns) {
  if (!columns || !columns.length) return row;
  const out = { _row: row._row };
  const wanted = columns.map(function (c) { return String(c).toLowerCase().trim(); });
  Object.keys(row).forEach(function (k) {
    if (wanted.indexOf(k.toLowerCase()) >= 0) out[k] = row[k];
  });
  return out;
}

function clip_(v) {
  const s = String(v);
  return s.length > MAX_CELL_CHARS ? s.slice(0, MAX_CELL_CHARS) + '…' : s;
}

function clampLimit_(n) {
  const v = parseInt(n, 10);
  if (!v || v < 1) return DEFAULT_LIMIT;
  return Math.min(v, MAX_LIMIT);
}

// ---------- Auth / output ----------

function isAuthorized_(e) {
  const key = PropertiesService.getScriptProperties().getProperty('ACCESS_KEY');
  if (!key) return false;
  const given = e && e.parameter && e.parameter.accessKey;
  return given === key;
}

function rpcResult_(id, result) {
  return { jsonrpc: '2.0', id: id, result: result };
}

function rpcError_(id, code, message) {
  return { jsonrpc: '2.0', id: id === undefined ? null : id, error: { code: code, message: message } };
}

function json_(obj) {
  return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}

function empty_() {
  return ContentService.createTextOutput('').setMimeType(ContentService.MimeType.JSON);
}

// ---------- Manual test (run from the editor) ----------

function testLocal() {
  console.log(JSON.stringify(handle_({ jsonrpc: '2.0', id: 1, method: 'tools/list' })).slice(0, 500));
  console.log(JSON.stringify(callTool_({ name: 'ops_overview', arguments: {} })).slice(0, 1500));
  console.log(JSON.stringify(callTool_({ name: 'ops_count_by', arguments: { column: 'status' } })));
}

appsscript.json

반드시 BOM 없이 저장하세요 (7-2의 PowerShell 명령 사용).

{
  "timeZone": "Asia/Seoul",
  "dependencies": {
    "enabledAdvancedServices": [
      {
        "userSymbol": "Sheets",
        "version": "v4",
        "serviceId": "sheets"
      }
    ]
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
    "executeAs": "USER_DEPLOYING",
    "access": "ANYONE_ANONYMOUS"
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets.readonly"
  ]
}

작성: 2026-09-28. Moonlight Ops v1.1 (읽기 전용 도구 4개) 기준.

Gemini Spark에 Exa 연결하기

AI용 검색엔진 Exa를 Spark 도구로 붙이는 초보자 튜토리얼

Exa는 AI가 쓰기 좋게 만든 웹 검색엔진입니다. 검색 결과를 링크 목록이 아니라 바로 읽을 수 있는 깔끔한 본문으로 돌려줍니다. Gemini Spark에 연결하면 조사, 트렌드 추적, 경쟁 분석 같은 일을 더 깊고 정확하게 맡길 수 있습니다.

난이도: 쉬움 (코드 없음, Exa 가입 1회). 걸리는 시간: 약 5분.

1. 완성하면 무엇이 달라지나

전에는

  • Spark가 웹을 찾아볼 수는 있지만, 검색 결과의 질이나 깊이를 조절하기 어려웠습니다.
  • 여러 사이트를 비교하는 조사는 검색 → 링크 열기 → 읽기 → 정리를 사람이 반복해야 했습니다.

이제는

  • @Exa로 AI용 검색을 직접 시킬 수 있습니다. 결과가 본문 형태로 와서 Spark가 바로 읽고 정리합니다.
  • 특정 URL 여러 개를 한 번에 읽혀서 비교하게 할 수 있습니다.
  • 로그인 방식으로 연결했기 때문에 **여러 단계 조사를 알아서 해 주는 agent_run**까지 쓸 수 있습니다.

2. 먼저 알아둘 개념

용어 쉬운 설명
Exa AI를 위한 검색엔진. 키워드가 아니라 “이런 페이지를 찾아줘”처럼 설명으로 검색하는 데 강합니다.
Exa MCP Exa 검색을 AI 도구로 쓸 수 있게 열어 둔 창구. 주소는 https://mcp.exa.ai/mcp
OAuth(로그인 연결) 비밀번호를 넘기지 않고 “이 앱이 내 계정을 써도 된다”고 허락하는 방식. 우리가 흔히 보는 “Google로 로그인”이 이 방식입니다.
크레딧 Exa 사용량을 계산하는 단위. 새 계정에는 무료 크레딧이 들어 있습니다.

3. 준비물

  • ☐ Gemini Spark를 쓸 수 있는 개인 구글 계정 (Google AI Pro/Ultra)
  • ☐ Gemini 설정에서 Keep Activity(활동 기록 유지) 켜기
  • ☐ Exa 계정 (없으면 연결 과정에서 Google 계정으로 바로 가입할 수 있습니다)

4. 연결 방식 3가지 이해하기

Exa MCP는 3가지 방식으로 쓸 수 있습니다.

방식 특징 쓸 수 있는 도구
키 없이(Keyless) 가입 없이 무료. 대신 사용량 제한이 있음 web_search_exa, web_fetch_exa
로그인(OAuth) Exa 계정으로 로그인. 사용량은 내 Exa 계정에 기록 위 2개 + agent_run
API 키 로그인 연결을 지원하지 않는 프로그램용 위와 같음

Spark에서는 로그인(OAuth) 방식으로 연결됩니다. 주소만 넣어도 Spark가 Exa의 로그인 절차를 자동으로 시작합니다. 결과적으로 가장 기능이 많은 형태로 연결됩니다.

5. 1단계: Spark에 주소 넣기

  1. https://gemini.google.com/spark/apps 로 갑니다. (Gemini 왼쪽 메뉴 Spark → Connected Apps)
  2. Custom apps 섹션에서 Add a custom app을 누릅니다.
  3. MCP Server URL에 아래 주소를 넣고 Next를 누릅니다.
    https://mcp.exa.ai/mcp
  4. “Connect this MCP server” 안내문을 읽고 “I understand and accept…” 체크 → Connect

6. 2단계: 구글 계정 연결 동의

Connect를 누르면 한국어로 이런 창이 뜹니다.

Google 계정과 [Custom] Exa 계정 연결하기 Google에서 다음 작업을 할 수 있습니다.

  • 모델 컨텍스트 프로토콜(MCP) 서버 도구 실행
  • [Custom] Exa에서 데이터 검색 및 관리
  • [Custom] Exa에 데이터 공유

내용을 확인하고 동의 및 계속을 누릅니다.

이 창은 “Gemini(Google)가 Exa 계정을 대신 써도 되는가”를 묻는 것입니다. Exa에 구글 비밀번호가 넘어가지 않습니다.

7. 3단계: Exa 로그인(가입)

페이지가 **Exa 로그인 화면(“Get started with Exa”)**으로 바뀝니다.

  1. Continue with Google을 누릅니다. (이메일로 가입해도 됩니다)
  2. 구글 계정 선택 화면에서 Spark와 같은 계정을 고릅니다.
  3. “Google will allow Exa to access this info about you”(이름, 프로필 사진, 이메일) 화면에서 Continue를 누릅니다.
  4. Exa 계정이 없었다면 이 과정에서 자동으로 가입되고 무료 크레딧이 들어갑니다.

로그인이 끝나면 자동으로 Gemini Spark 페이지로 돌아옵니다. 주소창에 custom_mcp_action=auth_complete가 보이면 인증이 끝난 것입니다.

8. 4단계: 저장하고 확인하기

  1. “Save your custom app” 창이 뜹니다.
    • App name: Exa
    • Available actions: web_search_exa, web_fetch_exa, agent_run
  2. Connect를 누릅니다.
  3. Custom apps 목록에 Exa가 켜진 상태로 보이면 성공입니다.
도구 하는 일 주로 쓰는 입력
web_search_exa 웹 검색 후 결과 본문을 깔끔한 텍스트로 가져옴 검색어(설명형), 결과 수
web_fetch_exa 알고 있는 URL(여러 개 가능)의 본문을 마크다운으로 읽음 URL 목록
agent_run 여러 번 검색하고 확인하는 다단계 조사를 알아서 수행. 목록 만들기, 조건 검증, 표 형태 결과에 강함 조사 요청

9. 5단계: 실제로 써보기

Spark 대화창에서 @Exa를 붙여 물어봅니다.

검색 요령: 키워드보다 “원하는 페이지를 설명”하세요.

  • 약함: AI 에이전트 교육
  • 강함: @Exa 초등 영어 교사가 수업에 AI 에이전트를 쓴 실제 사례를 소개한 블로그 글

조사

  • @Exa 이번 주에 공개된 오픈소스 AI 코딩 에이전트 출시 소식 찾아서 5줄 요약
  • @Exa Teachers Pay Teachers에서 잘 팔리는 novel study 상품의 공통 특징을 다룬 글 찾아줘

URL 읽기

  • @Exa 이 3개 페이지 읽고 가격과 기능 비교표 만들어줘: (URL1) (URL2) (URL3)

다단계 조사 (agent_run)

  • @Exa 한국 학원용 AI 도입 컨설팅 서비스를 하는 회사 10곳을 찾아서 이름, 사이트, 가격 공개 여부를 표로 정리해줘

다른 앱과 섞어 쓰기

  • @Exa 로 오늘 AI 뉴스 3개 찾아서 요약한 뒤 구글 문서로 저장해줘
  • 매일 오후 12시에 @Exa 로 새 AI 모델 출시 소식을 찾아서 내 Gmail로 보내줘 (Spark 예약 작업)

10. 이걸로 무엇을 할 수 있게 되었나

10-1. 더 정확한 조사

설명형 검색이라 “딱 이런 글”을 찾는 데 강합니다. 결과를 본문으로 받아서 Spark가 출처를 읽고 정리하므로, 링크만 나열하는 답보다 쓸모 있습니다.

10-2. 트렌드와 신규 출시 추적

AI 도구, 모델 출시처럼 빠르게 바뀌는 주제를 Spark 예약 작업과 묶으면 매일 자동 브리핑이 됩니다. SNS에 올릴 인사이트 글의 재료 모으기에도 좋습니다.

10-3. 시장과 경쟁 조사

TPT 같은 마켓의 경쟁 상품, 비슷한 서비스의 가격 정책, 사례 수집을 agent_run으로 표 형태까지 한 번에 받을 수 있습니다.

10-4. 여러 페이지 비교

web_fetch_exa로 URL 여러 개를 한 번에 읽혀 비교표를 만들 수 있습니다. 도구 카탈로그 정리, 서비스 비교 글 작성에 유용합니다.

10-5. 다른 앱과 조합

조합 할 수 있는 일
Exa + Moonlight Ops 내 시트의 시리즈 목록과 웹의 경쟁 상품을 비교
Exa + Hugging Face 뉴스로 새 모델을 찾고, HF에서 모델 카드 확인
Exa + DeepWiki 뉴스에 나온 오픈소스 레포의 실제 구조 확인
Exa + Google Docs/Gmail 조사 결과를 문서로 저장하거나 메일로 발송

11. 실제로 겪은 문제와 해결법

문제 1. “로그인 없이 된다더니 로그인 화면이 떠요”

  • 이유: Exa 서버는 키 없이도 동작하지만, 로그인 방식도 함께 알려 줍니다. Spark는 이걸 보고 자동으로 로그인 절차를 진행합니다.
  • 정리: Spark에서는 Exa 가입(또는 로그인)이 필요하다고 생각하면 됩니다. 대신 agent_run까지 쓸 수 있게 됩니다.

문제 2. 연결 도중 엉뚱한 확인창(“Google Workspace를 Gemini에서 연결 해제하시겠어요?”)이 떴어요

  • 상황: 저희 작업 중 한 번 Exa 연결 창과 겹쳐서 이 확인창이 떴습니다.
  • 해결: **Cancel(취소)**을 눌렀고, Google Workspace 연결은 그대로 유지됐습니다.
  • 교훈: 연결 중에 예상하지 못한 “연결 해제”, “Disconnect” 창이 뜨면 무조건 취소하세요.

문제 3. 로그인 후 원래 화면으로 안 돌아온 것 같아요

  • 확인: 주소창에 custom_mcp_action=auth_complete가 있으면 인증은 끝난 상태입니다. 잠시 기다리면 “Save your custom app” 창이 뜹니다.
  • 해결: 창이 안 뜨면 Connected Apps 페이지를 새로고침해서 Exa가 목록에 있는지 확인하세요. 없으면 5단계부터 다시 합니다.

그 밖에

증상 해결
Custom apps 메뉴가 없음 개인 계정인지, Spark 권한과 Keep Activity가 켜져 있는지 확인
agent_run이 안 보임 로그인 없이 연결된 경우입니다. 앱을 지우고 다시 연결해서 Exa 로그인까지 마치세요
결과가 엉뚱함 키워드 대신 “원하는 페이지를 설명”하는 문장으로 검색

12. 비용과 주의할 점

  • 무료 크레딧: 새 Exa 계정에는 무료 크레딧이 들어 있습니다. 로그인 방식에서는 사용량이 내 Exa 계정에 기록됩니다.
  • agent_run은 사용량 기반 과금입니다. 여러 번 검색하는 만큼 크레딧을 더 씁니다. 큰 조사를 자주 맡긴다면 https://dashboard.exa.ai 에서 남은 크레딧을 가끔 확인하세요.
  • 예약 작업 주의: 매일 도는 Spark 예약 작업에 agent_run을 넣으면 크레딧이 꾸준히 나갑니다. 일상 브리핑은 web_search_exa 위주로 요청하는 것이 경제적입니다.
  • 보내는 검색어는 외부 서버로 갑니다. Google은 맞춤 앱으로 연결한 외부 서버를 검토하거나 보호하지 않는다고 안내합니다. 개인정보나 내부 기밀을 검색어에 넣지 마세요.
  • 연결 해제: Connected Apps → Exa → More details → Disconnect 또는 Remove app

13. 더 나아가기: 고급 검색 도구 켜기

Exa에는 기본으로 꺼져 있는 **web_search_advanced_exa**가 있습니다. 도메인 지정, 날짜 범위, 카테고리, 지역 같은 세밀한 필터를 쓸 수 있습니다.

주소 뒤에 tools 값을 붙여서 켤 수 있습니다.

https://mcp.exa.ai/mcp?tools=web_search_exa,web_fetch_exa,web_search_advanced_exa,agent_run
  • tools를 적으면 기본 목록을 대체하므로, 쓰고 싶은 도구를 전부 적어야 합니다.
  • 적용하려면 Spark에서 기존 Exa 앱을 지우고 이 주소로 다시 연결합니다.
  • 참고: 이 확장 주소로 Spark에 연결하는 과정은 이번 작업에서 직접 테스트하지 않았습니다. 기본 연결이 잘 되고 있다면 필요할 때만 시도하세요.

14. 자주 묻는 질문

Q. Gemini에도 검색 기능이 있는데 왜 Exa를 붙이나요? Exa는 “설명형 검색”과 “본문 그대로 받기”에 특화돼 있어서, 깊은 조사나 여러 페이지 비교에서 차이가 납니다. 두 가지를 같이 두고 목적에 따라 @Exa를 지정해 쓰면 됩니다.

Q. Claude나 Cursor에서도 쓸 수 있나요? 네. 같은 주소 https://mcp.exa.ai/mcp를 각 도구의 MCP 설정에 넣으면 됩니다. 로그인을 원하면 https://mcp.exa.ai/mcp?login 주소를 씁니다.

Q. 가입한 Exa 계정은 어디서 관리하나요? https://dashboard.exa.ai 에서 사용량, 크레딧, API 키를 관리합니다.

작성: 2026-09-28. 실제 Spark 연결 과정(구글 계정 연결 동의, Exa Google 로그인)과 Exa 공식 문서 기준.

Gemini Spark에 DeepWiki 연결하기

깃허브 레포를 "읽어주는 AI 위키"를 Spark 도구로 붙이는 초보자 튜토리얼

DeepWiki는 공개 깃허브 레포를 자동으로 분석해서 위키(구조 설명, 문서, 다이어그램)를 만들어 주는 서비스입니다. 이걸 Gemini Spark에 연결하면, 코드를 직접 열지 않고도 “이 레포 어떻게 돌아가?”라고 물어볼 수 있습니다.

난이도: 가장 쉬움 (로그인, 가입, 코드 없음). 걸리는 시간: 약 3분.

1. 완성하면 무엇이 달라지나

전에는

  • 오래 안 건드린 레포를 다시 보려면 README를 읽고, 폴더를 뒤지고, 파일을 하나씩 열어야 했습니다.
  • 남의 오픈소스를 쓰려면 문서가 부족할 때 코드를 직접 읽어야 했습니다.

이제는

  • Spark에 “@DeepWiki 이 레포 구조 요약해줘”라고 물으면, DeepWiki가 미리 만들어 둔 위키를 근거로 답합니다.
  • “이 기능은 어느 파일에서 처리해?” 같은 질문에도 코드 기반으로 답을 받습니다.
  • Spark의 다른 앱(Docs, Gmail 등)과 묶어서 “레포 요약 문서 만들어줘” 같은 일도 맡길 수 있습니다.

2. 먼저 알아둘 개념

용어 쉬운 설명
DeepWiki Devin을 만든 Cognition이 운영하는 서비스. 공개 깃허브 레포를 분석해 위키를 자동으로 만들어 줍니다. deepwiki.com에서 볼 수 있습니다.
DeepWiki MCP 그 위키를 AI가 읽을 수 있게 열어 둔 “도구 창구”입니다. 무료이고 로그인이 필요 없습니다.
MCP AI와 외부 도구를 연결하는 표준 규격입니다. “AI용 USB-C 포트”라고 생각하면 됩니다.
Spark 맞춤 앱(Custom apps) MCP 서버 주소를 넣어 Spark에 도구를 추가하는 메뉴입니다.
색인(인덱싱) DeepWiki가 레포를 처음 분석해서 위키를 만드는 과정입니다. 색인이 안 된 레포는 질문해도 답이 안 나옵니다.

3. 준비물

  • ☐ Gemini Spark를 쓸 수 있는 개인 구글 계정 (Google AI Pro/Ultra)
  • ☐ Gemini 설정에서 Keep Activity(활동 기록 유지) 켜기
  • ☐ 끝. DeepWiki 쪽 가입이나 키는 필요 없습니다.

4. 1단계: 주소 확인하기

DeepWiki MCP 주소는 이것입니다.

https://mcp.deepwiki.com/mcp
  • 끝이 /mcp인 주소가 권장 방식(Streamable HTTP)입니다.
  • /sse로 끝나는 옛 주소도 있지만 곧 없어질 방식이라 쓰지 않습니다.

5. 2단계: Spark에 추가하기

  1. https://gemini.google.com/spark/apps 로 갑니다. (Gemini 왼쪽 메뉴 Spark → Connected Apps)
  2. 아래쪽 Custom apps 섹션에서 Add a custom app을 누릅니다.
  3. MCP Server URL 칸에 https://mcp.deepwiki.com/mcp를 붙여넣고 Next를 누릅니다.
  4. “Connect this MCP server” 안내문이 나옵니다.
    • Gemini가 이 서버의 도구를 실행하고, 데이터를 주고받을 수 있다는 내용입니다.
    • 맨 아래 “I understand and accept…” 체크박스를 누르고 Connect를 누릅니다.
  5. 잠시 로딩 후 “Save your custom app” 창이 뜹니다.
    • App name: DeepWiki (자동 입력)
    • Available actions: ask_wiki_question, read_wiki_contents, read_wiki_structure
  6. Connect를 누르면 끝입니다.

로그인 화면이 따로 뜨지 않는 게 정상입니다. DeepWiki는 인증 없는 공개 서버라서 연결이 가장 간단합니다.

6. 3단계: 연결 확인하기

Connected Apps 페이지의 Custom apps 목록에 이렇게 보이면 성공입니다.

DeepWiki   @DeepWiki   Last sync (연결한 날짜와 시간)   [켜짐]

More details를 누르면 도구 3개를 볼 수 있습니다.

도구 하는 일 필요한 입력
read_wiki_structure 레포 위키의 목차(주제 목록)를 가져옴 레포 이름 (예: facebook/react)
read_wiki_contents 위키 본문을 읽음 레포 이름
ask_wiki_question 레포에 대해 질문하면 위키 근거로 답함 레포 이름, 질문

레포 이름은 항상 소유자/레포 형식입니다. 예: Reasonofmoon/gov-scan-daily

7. 4단계: 내 레포를 DeepWiki에 등록(색인)하기

유명한 오픈소스(React, LangChain 등)는 이미 색인돼 있어서 바로 됩니다. 하지만 내 레포는 대부분 처음엔 색인이 안 돼 있습니다.

실제로 Reasonofmoon/gov-scan-daily를 조회해 보면 이런 답이 옵니다.

Repository not found. Visit https://deepwiki.com/Reasonofmoon/gov-scan-daily to index it.

색인하는 방법

  1. 브라우저에서 https://deepwiki.com/소유자/레포로 갑니다.
    • 예: https://deepwiki.com/Reasonofmoon/gov-scan-daily
  2. 색인(위키 생성) 요청 버튼이 나오면 누르고 안내를 따릅니다.
  3. 생성이 끝날 때까지 기다립니다. 레포 크기에 따라 몇 분 이상 걸릴 수 있습니다.
  4. 페이지에 위키가 보이면, 이제 Spark에서도 그 레포를 물어볼 수 있습니다.

큰 레포에서 중요한 폴더가 위키에서 빠진다면, 레포 루트에 .devin/wiki.json 파일을 두어 어떤 페이지를 만들지 직접 지정할 수 있습니다. (공식 문서의 “Steering DeepWiki” 참고)

8. 5단계: 실제로 써보기

Spark 대화창에서 @DeepWiki를 붙여 물어봅니다. 처음 도구를 쓸 때 Spark가 사용 허락을 물으면 허용합니다.

구조 파악

  • @DeepWiki facebook/react 위키 목차 보여줘
  • @DeepWiki Reasonofmoon/gov-scan-daily 전체 구조를 초보자 눈높이로 요약해줘

특정 질문

  • @DeepWiki langchain-ai/langchain 에서 retriever는 어떤 흐름으로 동작해?
  • @DeepWiki 이 레포에서 스케줄 실행은 어느 파일이 담당해? (Reasonofmoon/gov-scan-daily)

다른 앱과 섞어 쓰기

  • @DeepWiki 로 Reasonofmoon/gov-scan-daily 구조를 정리해서 구글 문서로 만들어줘
  • @DeepWiki 로 레포 3개(A, B, C)를 비교해서 표로 정리해줘

9. 이걸로 무엇을 할 수 있게 되었나

9-1. 오래된 레포 빠르게 복습

레포가 많을수록 “이거 뭐였지?”가 자주 생깁니다. 색인만 해 두면 README보다 자세한 구조 설명을 바로 받을 수 있습니다. 공개 레포 정리(아카이브 후보 고르기, 재활용할 코드 찾기)에 특히 좋습니다.

9-2. 오픈소스 도입 전 조사

새 라이브러리나 AI 도구를 쓰기 전에 “핵심 구조”, “설정 방법”, “확장 포인트”를 먼저 물어보면 문서만 볼 때보다 빠르게 판단할 수 있습니다. AI 도구 카탈로그에 새 도구를 소개할 때 근거 자료로도 쓸 수 있습니다.

9-3. 트렌드 레포 해설

깃허브 급상승 레포를 볼 때, 이름만 보고 넘기지 않고 “이 레포가 실제로 뭘 하는지 3줄 요약”을 받을 수 있습니다. (색인된 레포 기준)

9-4. 다른 도구와 역할 나누기

도구 잘하는 것
DeepWiki 공개 레포의 구조와 동작 원리 설명
Context7 라이브러리의 최신 공식 문서와 코드 예시
Github Personal Bridge 비공개 레포 포함 내 깃허브 파일과 이슈 직접 조회

같은 질문도 목적에 따라 알맞은 도구를 지정(@)하면 답이 더 정확해집니다.

10. 실제로 겪은 문제와 해결법

문제 1. 첫 연결에서 “We had trouble connecting to this server”

  • 증상: 주소를 넣고 Connect까지 눌렀는데 오류가 나며 입력 화면으로 돌아왔습니다.
  • 확인: 서버에 직접 요청을 보내 보니 DeepWiki는 정상 응답하고 있었습니다.
  • 해결: 같은 주소로 한 번 더 시도하니 바로 연결됐습니다. 일시적인 문제였습니다.
  • 교훈: 이 오류가 한 번 나왔다고 바로 포기하지 말고, 잠시 후 다시 시도해 보세요.

문제 2. 내 레포를 물어보면 “Repository not found”

  • 원인: DeepWiki에 아직 색인되지 않은 레포입니다.
  • 해결: 7장 방법으로 deepwiki.com에서 먼저 색인합니다.

그 밖에

증상 해결
Custom apps 메뉴가 없음 개인 계정인지, Spark 권한과 Keep Activity가 켜져 있는지 확인
비공개 레포가 안 됨 정상입니다. DeepWiki MCP는 공개 레포 전용 (11장 참고)
Spark가 DeepWiki를 안 씀 질문 앞에 @DeepWiki를 붙이고 레포 이름을 소유자/레포로 정확히 쓰기

11. 한계와 주의할 점

  • 공개 레포만 됩니다. 비공개 레포까지 쓰려면 Devin 계정을 만들고 API 키로 연결하는 별도의 Devin MCP가 필요합니다. 비공개 레포는 Github Personal Bridge 같은 다른 도구로 보는 편이 현실적입니다.
  • 위키는 색인 시점 기준입니다. 레포를 크게 바꿨다면 deepwiki.com에서 위키를 새로 만들게 해야 최신 내용이 반영됩니다.
  • AI가 만든 설명입니다. 중요한 결정(보안, 배포 설정 등)은 답에 나온 파일을 직접 열어 확인하세요.
  • 보내는 질문은 외부 서버로 갑니다. Google은 맞춤 앱으로 연결한 외부 서버를 검토하거나 보호하지 않는다고 안내합니다. 민감한 내부 정보를 질문에 넣지 마세요.

12. 자주 묻는 질문

Q. 돈이 드나요? 공개 레포용 DeepWiki와 DeepWiki MCP는 무료입니다.

Q. 연결을 끊고 싶어요. Connected Apps → DeepWiki의 More details → Disconnect 또는 Remove app을 누릅니다. 잠깐만 끄고 싶으면 목록의 스위치만 끄면 됩니다.

Q. Claude나 Cursor에서도 쓸 수 있나요? 네. 같은 주소 https://mcp.deepwiki.com/mcp를 각 도구의 MCP 설정에 넣으면 됩니다. 예: Claude Code는 claude mcp add -s user -t http deepwiki https://mcp.deepwiki.com/mcp

작성: 2026-09-28. 실제 연결 과정과 도구 목록 확인 결과 기준.