
Canvas, Moodle, Blackboard 같은 학습관리시스템(LMS)용 연동을 만들어본 적이 있다면, 그 고통을 알 것이다. 구체적으로는 **LTI 1.3(Learning Tools Interoperability)**의 고통이다.
OIDC 로그인, JWKS 키셋, RSA 서명, 프라이빗 키가 얽힌 복잡한 춤을 춰야 한다. 보안을 위해 설계된 건 맞지만, 그냥 "Canvas에 웹 앱 하나 넣고 싶다"는 개발자에게는 악몽처럼 느껴질 수 있다.
내가 정착한 아키텍처는 이렇다. 보안 계층에는 Node.js를, 프론트엔드에는 Next.js를 쓰고, 하나의 모놀리스로 만드는 대신 일부러 나눴다.
🔗 GitHub 저장소: github.com/paurushrai/lti-1.3-node-nextjs-template
🧐 LTI 1.3이란 무엇이고 왜 신경 써야 하는가
LTI(Learning Tools Interoperability)는 외부 툴(당신의 앱 같은)이 LMS(Canvas 같은)와 통신할 수 있게 해주는 표준이다.
과거의 LTI 1.1은 쉬웠다. "secret key"와 "consumer key"를 공유하면 끝이었다. 하지만 안전하지 않았다. LTI 1.3은 현대적인 정보 보안 모델로 넘어간다.
- 로그인 흐름을 위한 OIDC(OpenID Connect).
- 데이터 교환을 위한 JWT(JSON Web Token).
- 공유 비밀 대신 공개키/개인키 암호화(RSA256).
이 덕분에 엄청나게 안전해지지만, 처음부터 구현하기는 더 어려워진다.
🏗 아키텍처
이 문제를 풀기 위해 모놀리스를 만들지는 않을 것이다. 보안 프로바이더와 애플리케이션 UI를 분리할 것이다.
기술 스택
- 백엔드(경비원): Node.js + Express +
ltijs(Cvmcosta가 만든, LTI 1.3용 표준 Node.js 라이브러리). - 프론트엔드(경험): Next.js 14+(App Router) + React + Tailwind CSS.
- 데이터베이스: PostgreSQL(암호화 키와 플랫폼 등록 정보를 저장).
- 인프라: Docker Compose.
문제: "서드파티 쿠키" 대재앙
LTI 툴의 주요 난제는 LMS 안의 <iframe> 안에서 돌아간다는 점이다. 최신 브라우저(Safari, Chrome 시크릿 모드)는 기본적으로 서드파티 쿠키를 차단한다.
당신의 웹 앱이 세션 쿠키(connect.sid)에 의존한다면? Safari에서는 실패한다.
해결책: "LTIaaS" 토큰 흐름
이 프로젝트는 쿠키 제약을 우회하기 위해 "무상태" 토큰 패턴을 구현한다.
- LMS 실행: LMS가 우리 Node.js 백엔드로 요청을 보낸다.
- 핸드셰이크: 백엔드가 서명을 검증한다. 유효하면 짧은 수명의 일회용 **ltik(LTI key)**을 생성한다.
- 리다이렉트: 백엔드가 iframe을 Next.js 프론트엔드로 리다이렉트하면서, URL에 토큰을 담아 전달한다.
https://myapp.com/app?ltik=eyJhb... - 하이드레이션: Next.js 앱이 URL에서 이 토큰을 가져온다.
- API 호출: 프론트엔드는 이후 모든 API 호출의
Authorization헤더에 이 토큰을 Bearer 토큰으로 사용한다.
쿠키가 필요 없다!
🚀 설정 방법 (단계별)
로컬 머신에서 5분 안에 이걸 돌리는 방법이다.
1. 사전 준비물
Docker가 설치되어 있어야 한다. 그리고 ngrok(또는 비슷한 터널링 서비스)도 필요하다. LTI 툴은 HTTPS로 서빙되어야 하기 때문이다. localhost만으로는 안 된다.
2. 저장소 클론
git clone https://github.com/paurushrai/lti-1.3-node-nextjs-template.git
cd lti-1.3-node-nextjs-template3. 환경 설정
예시 환경 파일을 복사한다.
cp lti-server/.env.example lti-server/.env무작위 보안 키를 생성해서 .env의 LTI_KEY에 붙여넣는다.
4. 터널 시작
3000번 포트에서 ngrok을 시작한다(우리 Node 서버가 있는 곳이다).
ngrok http 3000https URL을 복사한다(예: https://my-tool.ngrok-free.app).
5. 앱 실행
lti-server/.env를 다시 열고 UI_URL을 ngrok URL로 설정한다.
이제 Docker로 전부 부팅한다.
docker-compose up --build이제 다음이 준비됐다.
- LTI 서버:
http://localhost:4000 - Next.js 클라이언트:
http://localhost:3000 - Postgres: 자동으로 연결됨.
🔌 LMS에 연결하기 ("마법" 링크)
예전에는 공개키와 URL을 수동으로 복사-붙여넣기 해야 했다. 이 프로젝트는 LTI Dynamic Registration을 지원한다.
- 등록 URL을 복사한다:
https://<your-ngrok-domain>/lti/register - LMS로 이동한다(예: Canvas Admin → Developer Keys).
- **"Dynamic Registration"**을 선택한다.
- URL을 붙여넣고 Register를 클릭한다.
끝. 💥 툴과 LMS가 자동으로 공개키를 교환한다. 이제 툴이 설치됐다.
🧪 흐름 테스트하기
- LMS 코스에서 일반 과제나 모듈 항목을 만든다.
- "External Tool"을 선택하고 새로 만든 툴을 고른다.
- 실행한다.
- iframe 안에서 Next.js 앱이 로드되는 걸 볼 수 있다.
- "Welcome, [당신의 이름]"과 역할(Instructor/Student)이 표시된다.
앱은 성공적으로 다음을 해냈다.
- 실행(launch)을 인증했다.
- LTI 실행 데이터에서 사용자 프로필을 디코딩했다.
- 이를 안전하게 프론트엔드로 전달했다.
보안 계층과 UI를 이렇게 나누는 방식에 동의하는가?
프로토콜 로직(Node/ltijs)을 애플리케이션 로직(Next.js)에서 분리한 것이 이 프로젝트를 다루기 쉽게 만든 핵심이다. 보안 계층은 지루하고 감사하기 쉬운 상태로 남고, UI는 OIDC나 JWKS를 전혀 몰라도 되는 평범한 Next.js 앱으로 남는다.
핸드셰이크를 이미 처리해주는 뭔가에서 시작하고 싶다면, 코드는 GitHub에 있다. 포크해서 써보라: lti-1.3-node-nextjs-template. 이렇게 두 계층을 나누는 방식이 도움이 되기는커녕 오히려 걸림돌이 된 경우를 겪은 사람이 있는지 궁금하다.


