나쁜 링크가 Production에 들어왔을 때
Vibe-coded 앱에서 deep link bug는 대체로 지루한 input 문제에서 시작합니다. malformed user ID, 오래된 campaign URL, push payload에서 빠진 enum 값 같은 것들입니다. 그다음 앱은 그 input을 신뢰해 버리고, happy-path logic 깊숙한 곳에서 screen이 crash합니다.
모든 URL parameter는 user input입니다. deep link, push notification payload, 공유 URL, QR code까지 전부 신뢰할 수 없습니다. Cursor로 Vibe coding을 하면 Claude Code는 happy path를 아주 잘 생성합니다. 하지만 paranoid path는 직접 만들라고 하지 않으면 나오지 않습니다. Claude는 가정합니다. Production은 그런 가정을 벌합니다.
Route Boundary에서 Zod로 검증하기
parameter는 앱 안으로 들어오는 지점에서 바로 validate해야 합니다. 검증이 끝나면, 나머지 코드는 typed되고 안전한 값만 다루게 됩니다.
// app/(app)/profile.tsx
// Validate at the gate. Everything past this point is typed.
import { useLocalSearchParams } from 'expo-router'
import { z } from 'zod'
const profileParamsSchema = z.object({
section: z
.enum(['overview', 'security', 'preferences'])
.default('overview'),
userId: z.string().uuid().optional(),
})
type ProfileParams = z.infer<typeof profileParamsSchema>
export default function ProfileScreen() {
const raw = useLocalSearchParams()
const result = profileParamsSchema.safeParse(raw)
if (!result.success) {
return <ErrorScreen message='Invalid profile parameters' />
}
const { section, userId } = result.data
return <ProfileContent section={section} userId={userId} />
}
ProfileContent는 이제 section과 userId를 올바르게 typed된 값으로 받습니다. useLocalSearchParams가 뭔지 몰라도 됩니다. validate도 하지 않습니다. 그저 정상 데이터만 받습니다. 현실 세계가 garbage를 보내 와도 AI가 생성한 feature가 망가지지 않는 Vibe-coded 앱은 이런 식으로 만듭니다.
+native-intent: 앱에 들어오기 전에 악성 URL을 거절하라
Expo Router는 malformed, stale, malicious할 수 있는 incoming native path를 처리하도록 +native-intent.tsx를 제공합니다. 이 파일은 provider가 mount되기 전에 실행됩니다. 여기서는 path를 rewrite하고 나쁜 입력을 거절하는 일만 해야 합니다.
// app/+native-intent.tsx
// This is your firewall, not your business logic.
export function redirectSystemPath({
path,
}: {
path: string
}) {
// Reject paths with suspicious patterns
if (path.includes('..') || path.includes('//')) {
return '/'
}
// Migrate old URL format to new format
if (path.startsWith('/user/')) {
const userId = path.replace('/user/', '')
if (z.string().uuid().safeParse(userId).success) {
return `/profile?userId=${userId}`
}
return '/'
}
return path
}
핵심은 이겁니다. +native-intent는 사용자가 로그인 상태인지 확인할 수 없습니다. 여기서 해야 할 일은 path sanitization뿐입니다. business authorization은 여기서가 아니라 route guard에 있어야 합니다.
Typed Route: 컴파일 타임에 오류 잡기
config에서 typedRoutes를 켜면, route name 오타를 사용자가 보기 전에 잡을 수 있습니다.
// app.config.ts
{
experiments: {
typedRoutes: true,
}
}
// Now the router validates at compile time:
router.push('/profile') // OK
router.push('/profle') // TypeScript error
router.push({
pathname: '/profile',
params: { section: 'overview' },
}) // OK
typed route는 route 이름 오타를 잡고, Zod validation은 잘못된 parameter를 잡습니다. 둘을 합치면 대부분의 웹 라우터가 제공하지 못하는 안전망이 생깁니다.
Autotomy Expo Starter Pack은 Zod validation pattern, typed route 설정, +native-intent handler를 모두 미리 갖추고 있습니다. deep link는 제대로 동작하고, 사용자는 크래시를 겪지 않습니다.