
GA Power Pages AI Skills 테스트 중: 더 나은 스캐폴딩(Scaffolding), 여전한 로컬 인증 장벽
요약
Microsoft의 Power Pages용 GitHub Copilot AI Skills를 사용하여 SPA를 구축하며 겪은 인증 구현 방식과 로컬 개발 시 발생하는 제약 사항을 다룹니다. AI가 생성한 스캐폴딩은 이전과 다른 인증 방식을 사용하지만, 로컬 환경에서 Private 설정 시 발생하는 리다이렉트 문제는 여전히 존재합니다.
핵심 포인트
- Power Pages AI Skills GA 버전은 더 넓은 엔드 투 엔드 작업 지원
- AI가 생성한 인증 방식은 커스텀 React 폼 및 세션 해결 포함
- 로컬 개발 시 Private 포털 설정은 Entra 리다이렉트 문제 유발
- 로컬 테스트를 위해서는 포털을 Public으로 설정해야 함
요약(TL;DR): Power Platform용 GitHub Copilot AI Skills를 사용하여 Power Pages SPA를 구축하는 과정에서, 이 포털은 이전 기사와는 완전히 다른 방식으로 인증을 처리합니다. 커스텀 React 폼, 서버 측 폼 스크래핑(form scraping), CSRF 토큰 가져오기, /_services/auth/user를 통한 세션 해결(session resolution) 등이 포함됩니다. 하지만 로컬에서 실행할 때 여전히 똑같은 장벽에 부딪힙니다. 포털이 "Private"으로 설정되어 있으면 모든 요청이 Entra를 통해 리다이렉트되며, 이때 redirect_uri가 프로덕션 URL을 가리키게 됩니다. 해결 방법은 동일합니다. 포털을 "Public"으로 설정하는 것입니다.
📖 배경: Power Pages SPA를 위한 AI Skills 실험
Microsoft는 최근 Power Pages 포털 구축을 위한 AI Skills를 GA(General Availability, 정식 출시)로 릴리스했으며, 이는 진지하게 다시 살펴볼 가치가 있게 만들었습니다. 이전 프리뷰(preview)와 비교했을 때, GA 버전은 더 많은 명령(commands), 더 광범위한 엔드 투 엔드(end-to-end) 작업, 그리고 Entra 이외의 인증 모델에 대한 훨씬 더 나은 지원을 제공합니다.
그래서 저는 도구를 제대로 이해하고 싶을 때 늘 하던 대로, Microsoft가 제공하는 Skills만을 사용하여 처음부터 포털을 구축해 보았습니다.
테스트 케이스는 **티켓 관리 포털(ticket management portal)**이었습니다. 외부 사용자가 등록하고, 티켓을 생성하며, 진행 상황을 추적하고, 지원 팀과 의견을 교환하는 방식입니다. **데이터 모델(data model)**은 제가 평소에 하던 방식인 PACX를 사용하여 구축했는데, 여전히 그 작업 단계에서는 그것이 훨씬 더 효과적이라고 생각하기 때문입니다. 나머지 부분인 포털, 인증, Web API 통합은 Skills를 통해 생성되었으며, 저는 주로 요구 사항을 정의하고, 계획을 검토하며, 각 단계를 승인하는 역할을 수행했습니다.
이 글은 약속했던 Skills 자체에 대한 심층 분석(deep dive)은 아닙니다. 대신 로컬 개발 중에 나타난 매우 구체적인 문제에 초점을 맞추고 있으며, 이 문제는 인증 구현 방식이 매우 다름에도 불구하고 제가 지난주에 설명했던 것과 정확히 동일한 근본 원인을 가지고 있음이 밝혀졌습니다.
🧱 이 포털이 인증을 처리하는 방식
AI 스킬(AI skills)은 제가 이전 기사에서 설명했던 직접 작성한 코드와는 의미 있는 수준으로 다른 로컬 인증(local authentication) 설정을 생성했습니다. 구현 방식의 선택이 직관적이지 않기 때문에 이를 살펴볼 가치가 있습니다.
아래의 모든 코드는 Copilot에게 다음과 같이 요청하여 스캐폴딩(scaffolded)되었습니다:
/setup-auth with local authentication and a basic registration form
🔐 로그인 양식 (The login form)
포털은 /login 경로에서 자체적인 커스텀 React 로그인 양식을 렌더링합니다.
Power Pages가 호스팅하는 로그인 페이지로의 리다이렉트(redirect)나 임베디드된 iframe(embedded iframe)은 없습니다. 양식은 클라이언트 측(client-side)에서 이메일과 비밀번호를 수집한 다음, 다음과 같은 다단계 시퀀스를 실행합니다:
1단계 — CSRF 토큰 가져오기 (Fetch a CSRF token).
export async function fetchAntiForgeryToken(): Promise<string> {
const response = await fetch('/_layout/tokenhtml');
// ...HTML 응답을 파싱하고 hidden input에서 value="..." 값을 추출합니다
...
Power Pages는 __RequestVerificationToken 필드를 포함하는 작은 HTML 조각인 /_layout/tokenhtml을 노출합니다. 이 토큰은 모든 변경(mutating) 요청에 포함되어야 하며, 그렇지 않으면 서버는 400 에러로 요청을 거부합니다.
2단계 — /SignIn으로 POST 요청 전송.
const body = new URLSearchParams();
body.set('__RequestVerificationToken', token);
body.set('Email', credential);
...
/SignIn은 Power Pages의 네이티브 로컬 인증 (local authentication) 엔드포인트입니다. 인증에 성공하면 세션 쿠키 (session cookie)를 설정하고 ReturnUrl로 리다이렉트 (redirect)합니다. React 핸들러는 이 리다이렉트를 감지하여 SPA를 그에 따라 탐색합니다.
3단계 — 사용자 신원(identity) 해결.
배포된 포털에서는 Power Pages의 서버 렌더링 (server-rendered) HTML에 의해 window.Microsoft.Dynamic365.Portal.User가 주입되어 즉시 사용할 수 있습니다. 하지만 Vite를 통해 로컬에서 실행할 때는 해당 객체가 존재하지 않습니다. Vite 개발 서버가 HTML을 제공할 뿐, Power Pages가 제공하는 것이 아니기 때문입니다.
이 간극을 메우기 위해 useAuth는 비동기 fetch (async fetch)로 대체됩니다. AI 스킬 (AI skills)은 처음에 이를 깔끔한 JSON 호출로 스캐폴딩 (scaffold)했습니다:
// AI 스킬이 생성한 코드 — 실제로는 작동하지 않음
const resp = await fetch('/_services/auth/user', { credentials: 'same-origin' });
const data = await resp.json(); // ❌ 오류 발생 — 응답이 JSON이 아님
하지만 /_services/auth/user는 JSON을 반환하지 않습니다. 대신 전체 HTML 페이지 — 즉, Power Pages 포털 셸 (shell) — 를 반환하며, 사용자 데이터는 <script> 블록 내부의 JavaScript 객체로 임베디드 (embedded)되어 있습니다:
<script>
window.Microsoft = window.Microsoft || {};
window.Microsoft.Dynamic365 = window.Microsoft.Dynamic365 || {};
...
경로가 명목상 404를 반환하더라도 (페이지 제목에 "Page not found"라고 표시됨), 세션이 인증된 상태라면 셸은 여전히 사용자 객체를 주입합니다. contactId의 존재 여부가 인증 신호입니다.
해결책은 정규 표현식 (regex)을 사용하여 HTML 응답을 파싱하도록 fetchCurrentUser를 다시 작성하는 것이었습니다:
export async function fetchCurrentUser(): Promise<PowerPagesUser | undefined> {
const platformUser = window.Microsoft?.Dynamic365?.Portal?.User;
if (platformUser?.userName) return platformUser;
...
Microsoft Learn에서 /_services/auth/user에 대한 문서를 찾을 수 없었습니다. 실제로는 테이블 권한 (table permissions)이 필요하지 않으며 Vite 프록시 (proxy)를 통해 접근할 수 있습니다. 지원되지 않는 방식으로 호출할 수 없는 것이 아니라, Microsoft 자체의 AI 스킬이 이 호출을 스캐폴딩한 것입니다.
📝 등록 (Registration)
등록(Registration)은 전적으로 **서버 측 폼 스크래핑 (server-side form scraping)**을 통해 처리됩니다. 이는 Power Pages 로컬 인증의 현실을 반영하는 기술입니다:
/Account/Login/Register에서 등록 페이지 HTML을 가져옵니다.- 이를 DOM 문서로 파싱하여 폼(form), 숨겨진 ASP.NET 필드(
__VIEWSTATE,__EVENTVALIDATION등), 그리고 위조 방지 토큰(anti-forgery token)을 추출합니다. - 사용자의 세부 정보를 포함하여 해당
actionURL로 폼을 POST 합니다. - 성공 시 리다이렉트(redirect)를 따르거나, 응답 HTML에서 서버 측 유효성 검사 오류를 파싱합니다.
등록을 위한 JSON API는 존재하지 않습니다. 포털의 기본 등록 폼은 내부적으로 ASP.NET WebForms로 구현되어 있으며, React SPA에서 이를 프로그래밍 방식으로 제어할 수 있는 유일한 방법은 상태를 스크래핑하고 재현(replay)하는 것입니다. AI 스킬은 이를 정확하게 수행했습니다. 제출하기 전에 라이브 폼을 조사(introspect)하며, 이는 구현체가 필드 이름이나 폼 액션의 변화를 하드코딩 없이 처리함을 의미합니다.
Vite 프록시 (The Vite proxy)
이 모든 것이 로컬에서 작동하도록 하기 위해, vite.config.ts는 관련 경로에 대한 프록시(proxies)를 정의하고 쿠키에서 Secure 및 Domain 속성을 제거합니다 (또한 SameSite=None을 SameSite=Lax로, 절대 경로인 Location 헤더를 상대 경로로 재작성합니다):
function rewriteProxyResponse(proxyRes) {
const setCookie = proxyRes.headers['set-cookie'];
if (setCookie) {
...
이는 nn-ticketing.powerappsportals.com에서 발행된 세션 쿠키가 http://localhost 환경의 브라우저에서 거부되는 것을 방지하기 위해 필요합니다.
참고: 프록시를 제대로 설정하기 위해 GitHub Copilot에게 요청해야 했습니다. 기본적으로, 기본 제공(ootb) AI 스킬은 다음과 같은 기본적인
vite.config.ts로 포털을 설정합니다:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
...
이는 제가 이전 포스트에서 이미 설명한 모든 문제점을 안고 있습니다. GH Copilot에게 적절한 프록시 관리(proxy management)를 추가해 달라고 요청한 후, vite.config.ts는 다음과 같이 변경되었습니다:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import type { IncomingMessage } from 'http';
...
🐛 문제점
이 모든 설정이 완료된 상태에서, npm run dev를 실행하고 http://localhost:5173/login으로 이동하면 React 로그인 양식이 나타납니다. 유효한 자격 증명을 입력하고 제출하면 다음과 같은 결과가 반환됩니다:
Failed to fetch
Playwright를 통해 네트워크 동작을 확인한 결과는 다음과 같습니다:
HTTP 302 http://localhost:5173/SignIn
HTTP 302 http://localhost:5173/_api/contacts?...
CONSOLE ERROR: Access to fetch at 'https://login.microsoftonline.com/...'
...
프록시된 모든 요청 — POST /SignIn, GET /_layout/tokenhtml, GET /_api/contacts — 이 https://login.microsoftonline.com으로 302 리다이렉트를 반환했습니다. 애플리케이션 코드는 전혀 실행되지 않았습니다.
리다이렉트 URL에는 다음이 포함되어 있었습니다:
redirect_uri=https%3A%2F%2Fnn-ticketing.powerappsportals.com%2F
해당 포털은 Power Pages 관리 센터에서 **"Private"**로 설정되어 있었습니다.
🔁 동일한 근본 원인, 다른 인증 스택
이전 기사에서 포털은 최소한의 인증 스택을 사용했습니다. 즉, window.Microsoft.Dynamic365.Portal.User에서 값을 읽거나 /_services/auth/user로 폴백(fallback)하는 fetchUser() 함수를 사용했으며, 로그인은 전체 페이지 네비게이션을 통해 /Account/SignIn으로 리다이렉트되었습니다. 구현 방식은 수동으로 작성되었으며 비교적 간단했습니다.
반면, 이 포털의 인증 스택은 실질적으로 훨씬 더 복잡합니다. 커스텀 React 양식, CSRF 토큰 가져오기, 서버 측 양식 스크래핑(form scraping), /SignIn으로의 SPA 네이티브 POST 요청, Vite 프록시에서의 쿠키 재작성(cookie rewriting), 그리고 비동기 fetchCurrentUser 폴백 등이 포함됩니다. AI Skills는 이 모든 것을 정확하게 생성해 냈습니다.
그 어떤 것도 중요하지 않았습니다. "Private" 플래그는 모든 엔드포인트 — /SignIn, /_layout/tokenhtml, /_services/auth/user, /_api/* — 의 상류(upstream)에 위치하며, 애플리케이션 수준의 로직이 실행되기 전에 동일한 Entra 게이트를 통해 이 모든 것을 가로챕니다. 인증 구현의 정교함은 무의미합니다. 포털이 "Private" 상태라면, Entra 리다이렉트(redirect)가 네트워크 수준에서 흐름을 차단하며, redirect_uri는 localhost가 아닌 프로덕션 URL을 가리키기 때문입니다.
로컬 개발에서 유일하게 중요한 변수는 다음과 같습니다:
Power Pages 관리 센터에서 포털이 "Public"으로 설정되어 있는가, 아니면 "Private"으로 설정되어 있는가?
"Private"인 경우: 모든 로컬 요청은 사용자가 재정의할 수 없는 redirect_uri와 함께 login.microsoftonline.com으로 302 리다이렉트됩니다.
"Public"인 경우: 모든 프록시된(proxied) 요청이 애플리케이션 계층에 도달하고, 쿠키가 올바르게 설정되며, 로컬 인증 흐름이 작동합니다.
✅ 해결책 (파트 1)
Power Pages 관리 센터 → 사이트 세부 정보(Site Details) → **"Private"**에서 **"Public"**으로 전환합니다. 그 후, 실제 서비스를 시작할 준비가 되면 https://make.powerpages.microsoft.com/ → 해당 사이트 → 사이트 가시성(Site visibility) → Public으로 설정합니다.
네트워크 계층에 대한 조치는 이것으로 끝입니다. 해당 설정을 변경한 후 포털이 활성화될 때까지 30분 정도 기다리면, 프록시된 요청이 애플리케이션에 도달하기 시작합니다.
🐛 두 번째 문제: isAuthenticated가 항상 false인 현상
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기


