ENFRKOantony langlois
프로젝트

XiLearn

바로가기웹사이트Next.jsNotion APIStripeMoodle

라오스를 위한 3개 국어 이러닝 플랫폼 · Notion을 유일한 데이터베이스로 쓰는 Next.js 스토어에서 강좌 판매 · 자체 호스팅한 Moodle에서 학습 진행 · 영어, 프랑스어, 라오어 Stripe 결제

아키텍처

Next.js client
프런트엔드 · 스토어
스토어 UI
src/app/[locale] · app router
강좌 목록, 상세 페이지, 결제 · Stripe PaymentElement 표시
SaaS
외부 · 결제
Stripe
Payment Intents · 브라우저에서 카드 확인
Node
코어 · Next.js 서버
App Router 서버
src/app · SSR + API routes
매 렌더마다 Notion 읽기 · /api/create-payment-intent · /api/addStudent
next-intl locale middleware서버 데이터를 클라이언트 컨텍스트로멱등 가입 쓰기hreflang + sitemap SEO비밀 키는 서버 사이드 전용
php · apache
서비스 · LMS
Moodle LMS
xilearn-moodle · Docker
자체 호스팅 · 분당 cron 하트비트 내장
SaaS
외부 · CMS + 백오피스
Notion 데이터베이스
강좌 · 카테고리 · 수강 등록 대기열(Need to be added)
localhost:8081
도구 · DB 관리
phpMyAdmin
개발 환경에서 Moodle 테이블 확인
Docker volume
데이터베이스 · LMS 데이터
MariaDB
mdl_* 테이블 · moodledata 볼륨의 파일

방문자가 /loStorefront UI에 접속하면 App Router 서버Notion에서 3개 국어 강좌 목록을 가져와 서버에서 렌더링합니다. 결제할 때 서버는 /api/create-payment-intent로 PaymentIntent를 만들고 브라우저는 Stripe Payment Element로 카드 결제를 확인합니다. /api/addStudent는 이메일과 강좌를 기준으로 중복을 제거한 뒤 학습자를 Need to be added 상태로 Notion 등록 대기열에 추가합니다. 운영팀은 이 대기열을 확인해 학습자를 Moodle에 수동으로 등록합니다. Moodle은 자체 Docker 스택에서 실행되며 데이터는 MariaDB에 저장합니다. 개발 중 데이터 확인에는 phpMyAdmin을 사용합니다.

배포: 스토어프론트 → Next.js · xilearn.com · LMS 스택 → Docker Compose · Moodle + MariaDB + phpMyAdmin

성과

  • 3개 로케일 · en / fr / lo · 라오 문자는 전용 폰트를 씁니다
  • 별도 관리 UI 없음 · Notion이 CMS, 백오피스, 등록 대기열 역할 담당
  • 2개 시스템 · 스토어프론트와 LMS가 독립적으로 배포됩니다
  • 1개 명령 · docker compose로 전체 LMS 스택 기동

보여준 역량: 헤드리스 CMS 데이터 모델링 · Stripe Payment Intents 생명주기 · 로케일 접두 SEO 라우팅 · PHP 모놀리스를 Docker에 패키징 · 멱등 쓰기 가드

문제와 해결책

핵심 제약은 3개 국어 강좌를 판매하는 소규모 팀이 맞춤형 백엔드를 구축하고 관리할 여력이 없다는 점입니다.

▩ 기성 LMS와 맞춤형 스토어 조합

문제: 진짜 강좌 플랫폼에는 레슨, 퀴즈, 진도 추적, 수료증이 필요합니다. 그것을 혼자 만들면 몇 년이 걸리지만, 기성품 Moodle은 낡아 보이고 마케팅, SEO, 카드 결제를 할 수 없습니다.

제품을 둘로 나눴습니다. Next.js 스토어가 강좌 탐색, 언어, 결제를 담당하고 자체 호스팅한 Moodle이 학습 기능을 담당합니다. 두 시스템은 수강 등록 단계에서만 연결됩니다.

  • 퀴즈와 진도 추적은 이미 잘 해결된 기능이라 맞춤형 LMS를 새로 만들지 않았습니다
  • Moodle을 마케팅 사이트로 꾸미면 모든 화면을 PHP 모놀리스에 맞춰야 하므로 제외했습니다
  • 배포 대상이 두 개로 늘고 수강 등록 단계의 연결 작업이 필요하다는 점은 감수했습니다

▦ CMS, 데이터베이스, 백오피스로서의 Notion

문제: 강좌 문구는 영어, 프랑스어, 라오어로 존재하며 비개발자가 편집합니다. 맞춤형 관리 패널이나 헤드리스 CMS 구독은 첫 강좌가 팔리기도 전에 하나의 프로젝트가 됩니다.

Notion 데이터베이스를 스토어의 유일한 데이터 저장소로 사용합니다. 강좌, 카테고리, 수강 신청을 팀이 매일 사용하는 테이블에서 관리하고 서버는 공식 API로 이를 읽습니다.

  • Contentful 같은 별도 SaaS를 도입하지 않아 편집자가 새 도구를 배울 필요도, 추가 비용도 없습니다
  • 한 행이 세 언어를 나란히 담아 번역이 서로 어긋나지 않습니다
  • 쿼리 성능과 API 응답 속도에는 한계가 있지만 작은 강좌 목록에서는 문제가 되지 않습니다

▧ Moodle API 브리지 대신 사람이 처리하는 등록 대기열

문제: 결제 후 학습자에게는 Moodle 계정과 강좌 등록이 필요합니다. Moodle 웹서비스 자동화는 설정이 취약하고, 초기에는 모든 결제가 사람의 눈길을 받을 가치가 있습니다.

결제가 끝나면 학습자를 Need to be added 상태로 Notion에 기록하고 운영팀이 대기열을 확인해 수동으로 등록합니다. 이메일 + 강좌 + 상태 중복 확인으로 재시도할 때 같은 레코드가 여러 번 생기지 않게 했습니다.

  • 현재 신청 규모에서는 사람이 몇 초면 처리할 수 있어 며칠이 걸리는 Moodle 웹서비스 연동을 제외했습니다
  • 수동 검토는 결제 검증도 겸합니다
  • 수강 권한이 즉시 열리지는 않지만 정해진 일정에 따라 진행하는 강좌에는 문제가 없습니다

▥ 호스팅형 리디렉션이 아닌 임베디드 Stripe 결제

문제: 구매자를 외부 결제 페이지로 보내는 것은, 온라인 카드 결제 자체가 이미 큰 도약인 시장을 대상으로 하는 사이트에서 신뢰를 깨뜨립니다.

서버는 결제마다 PaymentIntent를 만들고 브라우저는 Stripe의 Payment Element를 페이지 안에 렌더링합니다. 결제가 끝나면 /payment-success로 이동해 결과를 확인합니다. 비밀 키는 서버 밖으로 나가지 않습니다.

  • 결제 중 외부 페이지로 이동하면 신뢰를 떨어뜨릴 수 있어 Stripe 호스팅 Checkout은 제외했습니다
  • QR 코드 결제 경로를 프로토타이핑했다가, 현지 제공업체가 유지관리 가치를 가질 때까지 보류했습니다
  • 자동 결제 수단은 Stripe가 각 구매자에게 무엇을 제안할지 결정하게 합니다

▨ 검색 엔진이 색인할 수 있는 3개 국어 라우팅

문제: 라오 문자는 라틴 폰트에서 잘 렌더링되지 않고, 클라이언트 사이드 번역은 라오어와 프랑스어 카탈로그를 검색 엔진으로부터 숨깁니다.

next-intl 미들웨어가 hreflang 대체 링크와 함께 언어 코드가 포함된 URL(/en, /fr, /lo)을 제공합니다. 언어가 lo이면 레이아웃에서 Noto Sans Lao를 사용합니다. UI 문구는 JSON에, 콘텐츠 번역은 Notion 컬럼에 저장합니다.

  • 클라이언트 전용 i18n은 언어별로 검색 엔진이 색인할 URL을 만들기 어려워 제외했습니다
  • 로케일별 폰트 로딩은 모두에게 모든 폰트를 보내지 않고도 라오어 텍스트를 읽기 좋게 유지합니다
  • 세 언어를 지원하므로 모든 콘텐츠 필드가 CMS에 세 번씩 필요합니다

▣ 맞춤형 Docker 이미지 안에 소스로 빌드한 Moodle

문제: Moodle에는 정확한 PHP 확장, 쓰기 가능한 데이터 디렉터리, cron 하트비트가 필요합니다. 손으로 설치한 서버는 썩어가고 망가졌을 때 재구축할 수 없습니다.

맞춤형 php-apache Dockerfile에 필요한 PHP 확장, opcache, 1분 주기의 cron을 포함했습니다. config.php와 Moodle 소스는 볼륨으로 마운트하고 전체 스택은 compose 명령 하나로 시작합니다.

  • 미리 빌드된 Bitnami 이미지는 내부 구성을 파악하기 어렵고 정확한 Moodle 버전을 고정하기 힘들어 제외했습니다
  • 소스 체크아웃을 안정 브랜치에 고정해 업그레이드를 의도한 시점에만 진행합니다
  • 이미지를 직접 관리하는 대신 개발과 운영에서 완전히 같은 환경을 사용합니다