01왜 만들었나 — 교육을 '이벤트'가 아니라 '자산'으로
그룹사에 AWS 기반 데이터플랫폼이 들어왔다. 데이터를 다룰 수 있는 조직을 키우려면 각 계열사의 DCoE부터 교육해야 했고, 나는 그 교육 지원 역할을 맡았다. 처음엔 당연히 강의를 준비했다. 그런데 강의를 준비하면서 계속 걸리는 게 있었다.
강의는 이벤트다. 그날 그 자리에 있던 사람에게만 남고, 강사가 떠나면 지식도 함께 떠난다. 계열사가 여럿이고, 교육받을 사람은 계속 새로 생기는데, 매번 같은 강의를 반복하는 건 지속가능하지 않았다. 필요한 건 강의 한 번이 아니라, 교육 콘텐츠가 쌓이고 언제든 접근 가능한 자산이 되는 구조였다.
레퍼런스를 찾다가 AWS의 교육 사이트(Workshop Studio, Skill Builder 같은)를 벤치마킹했다. 거기서 배운 핵심은 콘텐츠와 플랫폼의 분리였다 — 잘 만든 플랫폼 위에 콘텐츠를 계속 얹으면, 교육은 저절로 축적된다. 그래서 목표를 다시 세웠다. 내가 강의 자료를 만드는 게 아니라, 누구나 콘텐츠를 얹으면 교육 사이트가 되는 프레임워크를 만들자. 이게 AX캠퍼스의 출발점이었다.
02근본 모순 — 정적 호스팅에서 동적 콘텐츠 관리
플랫폼을 설계하려니 곧장 근본적인 모순에 부딪혔다. 두 가지 요구가 정면으로 충돌했다.
- 운영은 가볍고 싸야 한다. 별도 서버를 세우고 관리할 여력이 없었다. 정적 호스팅(AWS Amplify)으로 가야 비용도 운영 부담도 최소였다.
- 제작자는 코딩을 몰라야 한다. 콘텐츠를 만드는 사람은 DCoE 담당자들이지 개발자가 아니다. 이들이 마크다운 파일 하나 추가하는 것만으로 새 교육 문서가 올라가야 했다.
문제는 이 둘이 기술적으로 충돌한다는 것이다. 브라우저는 파일 시스템에 접근할 수 없다. 정적 사이트는 public/markdown/ 폴더에 어떤 .md 파일이 있는지 런타임에 알 방법이 없다. 서버가 있으면 API로 파일 목록을 주면 되지만, 서버를 안 두기로 했으니 그 길은 막혀 있었다.
AX캠퍼스의 거의 모든 설계 결정은 이 하나의 모순 — "서버 없이, 그러나 파일만 추가하면 되는" — 을 푸는 과정에서 나왔다.
03핵심 설계 결정들
앞선 글들과 같은 방식으로, 각 결정을 상황 → 선택지 → 판단 → 근거로 정리했다. 근거는 실제 저장소의 코드와 ADR 문서 기준이다.
D1런타임에 못 할 일은 빌드타임으로 — Manifest 패턴
상황. 2장의 모순을 정면으로 마주하는 결정이다. 브라우저는 폴더를 스캔할 수 없는데, 파일 목록은 필요하다.
선택지. ① 서버 API로 파일 목록 제공(백엔드 필요, 비용 증가), ② 파일 목록을 사람이 수동 관리(파일 추가마다 코드 수정, 휴먼 에러), ③ 빌드타임에 폴더를 스캔해 목록을 JSON으로 굽는다.
판단. ③. 빌드 시 generate-manifest.js가 public/markdown/을 재귀 스캔해서, 각 파일의 경로·제목·카테고리·강의 ID를 뽑아 manifest.json으로 생성한다. 런타임에는 이 manifest만 읽으면 "어떤 문서가 있는지"를 즉시 안다. 런타임에 불가능한 일(파일 시스템 접근)을 빌드타임으로 옮긴 것이다.
// 빌드타임: 폴더 스캔 → manifest.json 생성
const files = findMarkdownFiles(markdownDir);
for (const filePath of files) {
manifest.push({ path, category, subCategory, title, ws_id });
}
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
이 결정의 진짜 값어치는 배포 워크플로에 있다. AWS Amplify가 main 브랜치 push를 감지해 자동으로 npm run build를 돌리고, 그때 manifest가 새로 구워진다. 즉 제작자는 마크다운을 git push만 하면, 빌드·manifest 생성·배포가 전부 자동으로 일어난다. 서버 없이도 "파일만 추가하면 되는" 경험이 완성된 것이다.
근거. ADR-001로 문서화. 대안(서버 API, 수동 관리)과의 비교표까지 저장소에 남겼다. 트레이드오프도 명시했다 — 로컬 개발 시에는 npm run generate:manifest를 수동으로 한 번 돌려야 한다. 자동화의 편의를 빌드 파이프라인에 의존하는 대가다.
D2제작자의 도구는 Obsidian이다 — 마크다운 + 프론트매터
상황. 콘텐츠 제작자(DCoE 담당자)가 어떤 도구로 글을 쓸 것인가. 이게 플랫폼 채택률을 좌우한다. 낯선 CMS를 새로 배우게 하면 아무도 안 쓴다.
판단. 제작자가 이미 익숙하거나 쉽게 배우는 Obsidian을 콘텐츠 저작 도구로 삼았다. 마크다운 본문 + YAML 프론트매터(태그 등 메타데이터) 구조라, Obsidian에서 글을 쓰고 이미지를 붙여넣으면 그대로 저장소 구조에 맞게 저장된다. 별도 편집기도, 별도 업로드 과정도 없다. 파싱은 gray-matter로 처리했다.
근거. ADR-002. Jekyll·Hugo 등과 같은 표준 프론트매터를 택해 호환성을 확보했고, 제작자용 Obsidian 설정 가이드(이미지 자동 저장 경로, 표준 마크다운 링크 설정)까지 README에 정리했다.
트레이드오프(솔직하게). gray-matter는 Node.js의 Buffer에 의존하는데 브라우저엔 Buffer가 없다. 그대로 쓰면 ReferenceError: Buffer is not defined로 죽는다. Vite의 node polyfill 플러그인으로 Buffer를 주입해 해결했다 — "익숙한 저작 경험"이라는 편의 하나를 얻기 위해, 브라우저 호환성이라는 숨은 비용을 지불한 셈이다.
D3강의는 여럿, 사이트는 하나 — ws_id 멀티테넌트
상황. 교육은 한 종류가 아니다. "데이터플랫폼 기초", "심화" 등 강의가 계속 늘어난다. 강의마다 사이트를 따로 만들면 관리가 N배가 된다. (MCP 프레임워크에서 계열사별 인스턴스를 두지 않았던 것과 똑같은 고민이다.)
판단. 단일 사이트 + ws_id(강의 ID) 기반 격리. URL을 /wiki/:wsId/:pageId로 두고, 폴더 구조 최상단을 강의 ID로 삼는다. 페이지 로드 시 현재 wsId에 속한 문서만 필터링해 보여준다. 하나의 배포, 하나의 코드베이스로 여러 강의를 굴린다.
// 현재 강의에 속한 페이지만 필터 (공통/시스템 페이지는 항상 노출)
export function filterPagesByCourse(pages, wsId) {
return pages.filter(page =>
!page.ws_id || page.ws_id === wsId || page.course === 'system'
);
}
근거. courseConfig.ts의 필터링 로직, public/markdown/ 하위의 강의 ID 폴더 구조(예: dcoe-basic-data-2025-...). 실제로 강의별 진행률도 progress-${wsId} 키로 분리 저장해, 학습자가 강의마다 독립적인 진도를 갖게 했다.
D4진짜 소비자는 '학습자'다 — 학습 경험을 위한 커스텀 렌더링
상황. 마크다운을 그냥 HTML로 렌더링만 하면 "문서 뷰어"에 그친다. 하지만 이건 교육 플랫폼이다. 학습자가 진도를 체크하고, 코드를 따라 치고, 어디까지 봤는지 아는 경험이 필요했다. (MCP 글에서 "도구의 진짜 소비자는 LLM"이라 했던 것과 같은 관점 — 여기선 소비자가 학습자다.)
판단. ReactMarkdown 위에 학습 경험용 커스텀 컴포넌트를 얹었다.
- 상태가 저장되는 체크리스트. 실습 단계를 체크하면
localStorage에 저장돼, 새로고침해도 진행 상태가 유지된다. 키는페이지ID-순번-텍스트조각으로 만들어 순서가 바뀌어도 고유성을 지켰다. - 자동 진행률 추적. 한 페이지에 20초 이상 머물면 "완료"로 기록하고, 강의 전체 진행률(%)을 계산한다. 학습자가 자기 진도를 눈으로 본다.
- 코드 블록 원클릭 복사, 이미지 라이트박스, 스크롤 spy 목차 — 실습을 따라 하는 데 필요한 마찰을 하나씩 없앴다.
근거. ADR-003. WikiContent.tsx의 20여 개 커스텀 컴포넌트, useAutoProgress 훅. 트레이드오프도 분명하다 — 진도·체크 상태를 localStorage에만 두니 기기를 바꾸면 진행률이 따라오지 않는다. 서버 없는 구조의 대가이고, 이 한계가 나중에 D5의 백엔드 도입을 부른다.
D5정적으로 시작했지만, 필요한 만큼만 서버리스로 — 수료 관리 백엔드
상황. 처음엔 완전 정적으로 충분했다. 그런데 교육이 돌아가자 새 요구가 생겼다 — 누가 어떤 강의를 수료했는지 관리하고, 수료증을 발급해야 했다. 이건 localStorage로는 안 된다. 기기를 넘어 지속되는, 관리자가 조회할 수 있는 진짜 데이터가 필요했다.
선택지. ① 상시 서버(EC2 등)를 세운다(다시 운영 부담·비용), ② 필요한 기능만 서버리스로 붙인다, ③ 외부 SaaS를 붙인다.
판단. ②. 처음의 "가벼운 운영" 원칙을 깨지 않으면서 딱 필요한 만큼만 백엔드를 붙였다. API Gateway + Lambda + DynamoDB 서버리스 조합이다. 프런트는 여전히 정적(Amplify), 상태가 필요한 기능(수료 신청·조회·수료증 발급)만 Lambda 함수로 처리한다.
수료 신청/조회/발급 → API Gateway → Lambda(Python) → DynamoDB (상태)
수료 상태 생성·조회·수정·삭제, 강의 목록, 카테고리, 수료증 발급까지 각각 독립된 Lambda 핸들러로 나눴다. 상시 켜둘 서버가 없으니 유휴 비용은 0에 가깝고, 트래픽이 몰려도 자동으로 확장된다.
근거. aws/lambda-functions/의 핸들러들(StatusCreate/List/Update/Delete, Certificate, Workshops, Categories), IAM 정책, 배포 스크립트. 프런트는 빌드 환경변수(VITE_API_BASE_URL)로 API Gateway 엔드포인트를 주입받는다.
돌아보면. "정적이냐 동적이냐"를 처음부터 이분법으로 결정하지 않은 게 핵심이었다. 정적으로 출발해 대부분을 싸게 유지하고, 상태가 진짜 필요해진 지점에서만 서버리스로 국소적으로 확장했다. 처음부터 풀스택 서버를 세웠다면 대부분의 기간 동안 쓰지도 않을 서버를 관리하고 있었을 것이다.
04플랫폼은 코드가 아니라 채택이 완성한다
AX캠퍼스도 앞선 프레임워크들과 같은 믿음 위에 있었다 — 만들어놓는 것과 남들이 쓰게 하는 것은 다른 일이고, 후자가 훨씬 어렵다.
그래서 채택 비용을 깎는 데 공을 들였다. 제작자는 낯선 CMS 대신 Obsidian으로 글을 쓰고(D2), git push만 하면 자동 배포된다(D1). 콘텐츠와 플랫폼이 분리돼 있으니, 새 강의가 생기면 폴더 하나 추가하는 것으로 끝난다(D3). "이 플랫폼에 기여하려면 무엇을 배워야 하는가"의 답을 최대한 "마크다운을 쓸 줄 알면 된다"에 가깝게 만든 것이다.
그 결과는 저장소가 증명한다. AX캠퍼스는 내 개인 프로젝트로 머물지 않았다. 300개 가까운 커밋이 쌓였고, 여러 기여자가 Pull Request로 참여하는 협업 저장소가 됐다. 데이터포탈 구성부터 Athena·QuickSight 실습까지 교육 콘텐츠가 실제로 축적됐다. 강의 한 번이 아니라, 계속 자라는 교육 자산이 된 것이다 — 이게 처음에 세운 목표 그대로였다.
더 나아가, AX캠퍼스는 데이터플랫폼 교육을 넘어 다른 프로젝트의 지식까지 실어 나르는 그릇이 됐다. 앞선 글에서 다룬 MCP 프레임워크의 셋업·테스트·배포 가이드도 이 플랫폼에 콘텐츠로 담았다. 프레임워크를 만들고(MCP 글), 그 사용법을 교육 플랫폼에 얹어(이 글) 다른 팀이 배우게 하는 — 두 프로젝트가 하나의 흐름으로 이어진 것이다. 그리고 지금 AX캠퍼스는 내 손을 떠나, 사내 LLM 플랫폼 AI 아틀라스를 비롯한 여러 곳에서 교육·문서 플랫폼으로 실제 활용되고 있다. 개인이 문제를 풀려고 만든 도구가, 그룹의 공용 자산이 된 셈이다. 플랫폼은 코드가 아니라 채택이 완성한다는 말의, 가장 분명한 증거다.
05돌아보며 — 세 번째로 반복된 같은 방식
이 글을 쓰면서 스스로도 새삼 확인한 게 있다. AX캠퍼스에서 내가 한 일은, 사실 이전에 했던 것과 똑같은 모양이었다.
SVN → Git 이주에서는 감사라는 문제에서 출발해 내 시스템을 마루타로 삼아 브랜치 전략을 설계하고 자동화했다. MCP 프레임워크에서는 표준 부재라는 문제에서 출발해 내가 첫 유저가 되어 프레임워크를 실증하고 전파했다. 그리고 AX캠퍼스에서는 "지속가능한 교육"이라는 문제에서 출발해, 정적/동적의 모순을 풀고, 제작자가 쉽게 기여하는 플랫폼을 만들어 확산시켰다.
세 번 다 같은 흐름이다. 현실의 문제를 정의하고 → 도구가 아니라 흐름을 설계하고 → 사람이 쉽게 올라탈 수 있게 만들고 → 남들이 따라올 길을 닦는다. 도구는 형상관리에서, MCP로, 교육 플랫폼으로 계속 바뀌었지만 일하는 방식은 그대로였다. 새 기술이 유행해서 방식이 생기는 게 아니라, 방식이 먼저 있고 그 위에 새 도구가 얹힌다는 걸 나는 이 세 프로젝트에서 반복해서 확인했다.
그리고 이 방식이 가장 흥미로운 형태로 나타나는 곳이 남아 있다. 지금까지가 "도구와 콘텐츠를 서빙하는 플랫폼"이었다면, 그 위에서 스스로 판단하고 행동하는 AI 에이전트(오딘)는 전혀 다른 종류의 문제를 던졌다. 자율성과 결정론 사이에서 아키텍처를 어떻게 수렴시켰는지 — 그 이야기를 다음 글에서, 이 시리즈의 마지막으로 다루려 한다.