약 16분 읽기
청년정책이 흩어져 있어서
청년들이 받을 수 있는 정부 지원 정책은 여러 곳에 흩어져 있다. 중앙정부에서 나오는 정책도 있고, 각 지자체에서 제공하는 정책도 있어서 한눈에 보기 어렵다. 정책마다 지원 자격과 혜택이 다르기 때문에, 각각의 정보를 일일이 찾아보고 비교하는 건 번거로운 일이다. 이런 이유로 많은 청년이 자신에게 맞는 정책을 놓치고 있다.
특히 연령과 지역 조건이 복잡하게 얽혀 있는 경우가 많다. 나이 조건만 해도 ‘만 19세~34세’처럼 범위로 주어지기도 하고, ’19~34’처럼 생략되기도 한다. 어떤 경우엔 ‘만19세 이상 만35세 미만’처럼 자세하게 나와 있어도, 읽고 해석하는 데 시간이 걸린다. 이런 복잡함을 해결하기 위해 연령 조건을 정형화해서 제공할 필요가 있었다.
문제는 혼란스러운 정보뿐만이 아니다. 정책의 실질적인 혜택을 이해하는 것도 쉽지 않다. 예를 들어, 소득 조건은 ‘중위소득 150% 이하’로 주어지는 경우가 많다. 하지만 중위소득이란 기준은 매년 변동되고, 사람마다 소득 구성도 다르다. 이 때문에 기준중위소득 표를 따로 만들어서 참고할 수 있도록 제공했다. 청년들이 실제로 어떤 혜택을 받을 수 있는지를 쉽게 확인할 수 있도록 해야 했다.
청년정책을 한 곳에서 확인할 수 있는 플랫폼이 필요하다는 생각에서 이 프로젝트를 시작했다. 이처럼 흩어져 있는 정보를 모아 정리하는 작업은 중요하지만, 어떤 부분부터 시작해야 할지 막막한 점도 있었다. 다양한 정책을 수집하고 정리하는 데 어려움이 있었지만, 그 과정에서 청년들이 직면한 문제를 더 깊이 이해할 수 있었다.
미니앱은 일반 웹앱과 다르다
미니앱은 일반 웹앱과는 다른 몇 가지 특성을 지닌다. 우선, 번들링 단계에서 일반 웹앱과 차이가 있다. 일반 웹앱은 주로 .zip이나 .tar 형식으로 번들링되지만, 미니앱은 .ait 파일로 번들링되어야 한다. 이 .ait 파일은 토스 플랫폼의 콘솔에 업로드하여 배포하게 되며, 이는 플랫폼의 통제된 환경에서 동작하기 위한 필수적 과정이다. 번들링 방식의 차이로 인해 초기 설정 과정에서 혼란스러웠으나, 반복적인 작업을 통해 익숙해졌다.
네비게이션 바 설정도 일반 웹앱과 차별화된 부분이다. 미니앱은 자체적으로 네비게이션 바를 그리지 않고, defineConfig를 통해 네비게이션 바를 설정하는 방식이다. 이 과정에서 필수적인 요소로는 좌측의 뒤로 가기 버튼과 중앙의 로고 및 앱 이름이다. 이러한 설정이 요구되는 이유는 미니앱이 토스 내 다른 서비스와 시각적으로 통일감을 유지해야 하기 때문이다. 나는 처음 심사에서 이 부분에서 문제가 되어 설정을 수정해야 했다.
토스 디자인 시스템(TDS)을 활용하는 것도 미니앱 개발의 중요한 특징이다. TDS를 사용함으로써 미니앱은 토스의 다른 서비스와 시각적 일관성을 유지할 수 있다. 디자인 측면에서 토스 플랫폼의 사용자들은 이질감 없이 서비스를 이용할 수 있다는 장점이 있다. 개인적으로는 처음 사용해보는 디자인 시스템이었기에 적응하는 데 시간이 걸렸지만, 결과적으로는 일관된 사용자 경험을 제공할 수 있었다.
마지막으로, 미니앱은 일반 웹앱과 다르게 심사 과정을 거친다. 이는 플랫폼 내에서의 일관성을 유지하기 위한 절차로, 필수적인 요소들이 제대로 구현되어 있는지를 검토한다. 처음 심사에서 네비게이션 바 문제로 반려된 경험은, 미니앱 개발 시 플랫폼의 가이드라인을 철저히 파악하고 준수해야 함을 깨닫게 해줬다. 이러한 과정은 다소 번거로웠지만, 플랫폼의 통일성과 사용자 경험을 고려한 필수적인 절차라는 점에서 수긍할 수 있었다.
첫 심사에서 반려된 이유
초기에는 네비게이션 바를 앱 화면에서 직접 그리는 방식으로 구현했다. 사용자 경험을 고려해 커스텀 디자인을 적용하려는 목적이었다. 그러나, 이는 토스 플랫폼의 가이드라인을 제대로 반영하지 못한 결과였다. 심사에서 반려된 이유는 네비게이션 바의 필수 요소인 좌측 뒤로가기 버튼과 중앙 로고+앱 이름 구성을 설정으로 선언하지 않았기 때문이다. 이러한 커스텀 접근은 플랫폼의 통일성을 저해할 수 있어, 심사 기준에 부합하지 않았다.
// apps-in-toss.config.ts
import { defineConfig } from '@apps-in-toss/web-framework/config';
export default defineConfig({
appName: 'policymate',
brand: {
primaryColor: '#3D5AFE',
},
permissions: [], // 안 쓰는 권한은 비워둬야 심사가 빠르다
navigationBar: {
withBackButton: true, // 좌측 뒤로가기 — 심사 필수
withTitle: true, // 중앙 로고+앱이름 — 심사 필수
},
webBundleDir: 'dist',
});
// 처음엔 커스텀 헤더를 직접 그렸다가 반려됐다.
// 여기서 선언하고 화면에서는 안 그리는 게 맞다.
네비게이션 바는 설정으로 선언한다 — 직접 그리면 심사에서 걸린다
설정 파일의 navigationBar 정의를 통해 필수 요소들을 선언해야 했다는 것을 알게 되었다. 에서 이러한 설정을 반영함으로써, 플랫폼의 일관된 사용자 경험을 유지할 수 있게 됐다. 결과적으로, 이 변경을 통해 심사를 통과할 수 있었다. 이 과정은 다소 번거로웠지만, 플랫폼의 기준을 준수하는 것이 얼마나 중요한지를 직접 체감할 수 있었다.
나이 조건 표기가 지자체마다 다르다
청년정책의 나이 조건은 지자체마다 제각각이라 이를 표준화하는 데 애를 먹었다. 어떤 지자체는 ‘만 19세~34세’라고 명시하고, 또 다른 곳은 ‘만19세 이상 만35세 미만’처럼 표현한다. 이렇게 다양한 표현 방식 때문에 데이터를 일관되게 처리하기 위해 조건을 정수로 변환해야 했다. 이러한 정수 변환이 없으면 사용자가 입력한 나이와 정책의 나이 조건이 비교될 수 없다는 문제가 발생했다. 그래서 각기 다른 형식의 나이 조건을 일관된 형태로 정규화하는 코드가 필요했다.

AGE_PATTERNS = [
# '만 19세 ~ 34세', '19~34세', '만19세이상 만35세미만' 전부 다르게 온다
(re.compile(r'만?\s*(\d{2})\s*세?\s*[~\-–]\s*만?\s*(\d{2})\s*세'),
lambda m: (int(m.group(1)), int(m.group(2)))),
(re.compile(r'만?\s*(\d{2})\s*세\s*이상.*?만?\s*(\d{2})\s*세\s*미만'),
lambda m: (int(m.group(1)), int(m.group(2)) - 1)),
(re.compile(r'만?\s*(\d{2})\s*세\s*이하'),
lambda m: (0, int(m.group(1)))),
(re.compile(r'만?\s*(\d{2})\s*세\s*이상'),
lambda m: (int(m.group(1)), 99)),
]
def parse_age_range(raw: str):
s = raw.replace(' ', '')
for pat, fn in AGE_PATTERNS:
m = pat.search(s)
if m:
lo, hi = fn(m)
return {"min_age": lo, "max_age": hi}
return {"min_age": None, "max_age": None} # 못 읽으면 비워둔다
# 소득 조건은 기준중위소득 표가 따로 필요하다
MEDIAN_INCOME_2026 = {1: 2_392_013, 2: 3_932_658, 3: 5_025_353, 4: 6_097_773}
def income_ceiling(household: int, percent: int) -> int:
return int(MEDIAN_INCOME_2026[min(household, 4)] * percent / 100)
지자체마다 다른 나이 조건 표기를 정수로 정규화
정규식을 활용해 다양한 나이 조건을 정수로 변환하는 함수를 작성했다. 이 과정에서 가장 큰 도전은 ‘만’이라는 단어의 의미를 정확히 파악하는 것이었다. ‘만 19세~34세’는 ‘만 19세 이상 만 35세 미만’과 동일한 범위를 나타내기 때문에, 이를 같은 값으로 변환해야 했다. 다행히 정규식을 잘 활용하면 이러한 복잡한 조건문을 간단한 패턴으로 처리할 수 있었다. 이렇게 정규화된 데이터를 사용함으로써 정책 검색 기능의 정확도를 크게 향상시킬 수 있었다.
화면을 3개로 제한한 이유
정책 메이트 애플리케이션은 사용자 경험을 단순하고 직관적으로 유지하는 것이 중요했다. 미니앱의 특성상 사용자가 머무르는 시간이 짧기 때문에, 불필요한 화면 전환이나 복잡한 탐색 과정을 최소화해야 했다. 초기 설계 단계에서 화면 수를 늘리면 사용자가 원하는 정보를 찾기까지의 과정이 복잡해질 수 있다는 문제점이 있었다. 따라서, 온보딩에서 나이와 지역 정보를 입력받고, 결과 리스트 및 상세 정보 화면으로 바로 연결되는 3단계 구조를 채택했다. 이렇게 함으로써 사용자는 최소한의 클릭으로 원하는 정책 정보를 빠르게 확인할 수 있었다.
또한, 사용자가 중간에 관심을 잃고 앱을 종료하는 것을 방지하기 위해 화면 설계에 많은 고민을 했다. 화면이 많아질수록 사용자는 탐색에 시간을 더 소비해야 하며, 이는 사용자 경험을 저해할 수 있었다. 따라서 필수적인 정보만을 제공하면서도 사용자에게 만족스러운 경험을 줄 수 있는 방법을 모색했다. 이 과정에서 각 화면이 명확한 목적을 가지고 있어야 했고, 사용자가 다음 단계로 자연스럽게 넘어갈 수 있도록 구성했다.
// App.tsx — 온보딩 → 결과 → 상세. 라우터도 안 쓴다
type Screen =
| { name: 'onboarding' }
| { name: 'results'; profile: Profile }
| { name: 'detail'; policy: Policy; profile: Profile };
export default function App() {
const [screen, setScreen] = useState<Screen>({ name: 'onboarding' });
switch (screen.name) {
case 'onboarding':
return (
<Onboarding
onSubmit={(profile) => setScreen({ name: 'results', profile })}
/>
);
case 'results':
return (
<ResultsList
profile={screen.profile}
onSelect={(policy) =>
setScreen({ name: 'detail', policy, profile: screen.profile })
}
/>
);
case 'detail':
return (
<PolicyDetail
policy={screen.policy}
onBack={() => setScreen({ name: 'results', profile: screen.profile })}
/>
);
}
}
화면은 3개로 끝낸다 — 미니앱은 체류시간이 짧다
정책 메이트의 구조는 사용자 피드백을 통해 몇 차례 조정이 있었다. 초기에 비해 불필요한 요소를 많이 제거했고, 사용자 테스트를 통해 화면 전환의 매끄러움을 지속적으로 개선했다. 결국 화면을 3개로 제한한 것은 매우 빠른 선택으로 판명되었으며, 사용자들이 앱을 사용하는 데에 부담을 느끼지 않게 만들었다. 이러한 접근 방식 덕분에 사용자 만족도도 높아졌고, 미니앱이라는 특성에도 잘 맞았다.
빌드와 심사 제출
policymate 앱은 일반적인 웹앱과 다르게, 앱인토스 플랫폼의 독특한 빌드 과정을 거쳐야 했다. 먼저, 앱 번들을 .ait 파일 형식으로 빌드한 후 이를 토스 콘솔에 업로드했다. 이 과정에서 가장 짚어둘 건 번들이 앱인토스 플랫폼의 요구 사항을 정확히 반영하는 것이었다. 특히, TDS(토스 디자인 시스템)을 적극 활용하여 토스 환경과 일관된 디자인을 유지하기 위해 노력했다. 이러한 디자인 요소는 심사 과정에서 매우 중요한 부분을 차지했다.
심사 제출 초기에는 네비게이션 바 설정 문제로 반려를 받았다. 초기에는 커스텀 헤더를 직접 그려 넣었지만, 이는 플랫폼의 네이티브 설정을 따르지 않은 것이었다. defineConfig의 navigationBar 설정을 사용하여 필수 요소인 좌측 뒤로가기 버튼과 중앙 로고, 앱 이름을 포함시키는 방향으로 수정했다. 이 변경 사항은 심사 통과에 큰 영향을 주었고, 이후 심사 과정은 비교적 순조롭게 진행됐다.
데이터 정규화 작업 또한 빌드와 심사 제출에 중요한 역할을 했다. 다양한 지자체의 청년 정책 정보를 정형화하는 과정에서, 나이와 소득 조건을 통일된 형식으로 변환했다. 이런 데이터 준비 과정은 앱의 기능성을 높이는 데 기여했고, 정책 매칭의 정확도를 향상시켰다. 심사에서는 이러한 데이터 정합성과 기능성이 긍정적인 평가를 받아 제출 과정이 원활하게 이루어졌다.
마지막으로, 백엔드와의 연동 검증도 필수적이었다. policy-api 컨테이너를 통해 홈서버에 올린 백엔드가 프론트엔드와 제대로 작동하는지 확인했다. 이 과정에서 발생한 이슈들은 빠르게 해결했고, 전반적인 시스템 안정성을 확보했다. 이러한 준비 과정 덕분에 bimni앱 심사가 통과되었고, 출시 단계로 넘어갈 수 있었다.
만들고 나서
프로젝트를 마무리한 후, 가장 먼저 느낀 점은 플랫폼 특성에 맞춘 개발의 어려움이었다. 특히 TDS를 활용한 디자인 작업에서는 기존의 웹앱 개발과 다른 기준을 맞춰야 했다. 처음에는 익숙하지 않았다. 하지만 이는 토스 내의 일관된 사용자 경험을 위한 중요한 요소라는 것을 깨달았다. 디자인 시스템과의 마찰을 줄이는 방법을 고민하면서, 결국에는 TDS에 대한 이해도가 높아졌다.
또한, 정책 데이터를 정형화하는 과정에서 예상치 못한 난관들이 있었다. 예를 들어, 다양한 표현 방식으로 기재된 나이 조건은 정규식으로 파싱해 통일했지만, 생각보다 복잡한 경우가 많았다. 모든 경우를 고려해 정규식 패턴을 조정하는 데 시간이 소요됐다. 이런 과정을 통해 데이터 정형화의 필요성을 다시 한 번 체감했다. 결국, min_age와 max_age로 명확하게 정리할 수 있었다.
프로젝트를 진행하면서 가장 큰 보람은 사용자 피드백이었다. 앱을 통해 청년 사용자들이 놓치고 있던 정책을 알게 되었다는 이야기를 들었을 때, 초기 기획 의도가 제대로 전달된 것 같아 뿌듯했다. 사용자가 느끼는 만족감이 큰 동기부여가 됐다. 반면, 일부 피드백에서는 결과 리스트의 편의성을 개선해달라는 요청이 있었다. 이를 반영해 이후 업데이트 때 리스트의 필터링 기능을 더 강력하게 만들 계획이다.
마지막으로, 정책 데이터의 최신성을 유지하는 것도 큰 과제였다. 크롤링을 통한 데이터 수집은 자동화했지만, 지자체의 공고 변경에 대한 대응이 필요했다. 예측할 수 없는 변수들이 많아 지속적인 모니터링이 요구됐다. 이 부분은 개발 후에도 계속 해결해 나가야 할 부분이다. 단순한 기능 추가보다 데이터의 신뢰성 확보가 장기적으로 더 중요한 작업이라는 생각이 들었다.
함께 볼 글
이 글과 이어지는 내용은 아래에 정리해뒀다.