프론트엔드 AI 활용은 두 갈래다
같은 검색어 뒤에 사실 두 개의 질문이 숨어 있다. 하나는 "AI로 코드를 더 빨리 짜는 법", 다른 하나는 "AI 기능이 들어간 화면을 만드는 법"이다. 둘은 필요한 지식이 거의 겹치지 않는다.
| 구분 | AI로 코드를 짠다 | AI 기능을 화면에 붙인다 |
|---|---|---|
| 핵심 고민 | 어떤 도구를 쓸까, 어떻게 프롬프트할까 | 느리고 불확실한 응답을 어떻게 보여줄까 |
| 바뀌는 것 | 개발 속도와 작업 습관 | UI 상태 설계, 에러 처리, 비용 구조, 보안 경계 |
| 실패하면 | 생산성이 기대보다 안 오른다 | 사용자가 멈춘 화면을 보거나, 청구서가 튀거나, 키가 샌다 |
| 학습 곡선 | 비교적 완만 — 도구를 바꿔 쓰면 된다 | 가파름 — 기존 UI 패턴을 다시 짜야 한다 |
앞쪽은 이미 다룬 적이 있다. 도구 선택과 프레임워크별 활용은 AI 기반 코드 생성 가이드에서 이어진다. 이 글은 뒤쪽, 그러니까 AI를 제품 안에 넣을 때 화면에서 벌어지는 일만 다룬다.
AI 기능의 프론트엔드는 결국 느리고, 길이를 모르고, 실패할 수 있고, 유료인 네트워크 요청을 다루는 문제로 환원된다. 이 네 가지를 UI 어디에서 흡수할지 정하는 것이 설계의 대부분이다. 나머지는 늘 하던 프론트엔드 작업이다.
스트리밍 UI — 첫 글자까지가 승부처
일반적인 API는 응답이 한 번에 온다. 로딩 스피너를 띄우고 기다리면 된다. LLM 응답은 수 초에서 수십 초가 걸리는데, 같은 방식으로 처리하면 사용자는 그 시간 내내 빈 화면을 본다.
스트리밍은 전체 소요 시간을 줄여주지 않는다. 다만 첫 글자가 보이는 시점을 앞당겨 체감 대기를 크게 줄인다. 더 중요한 건 사용자가 초반 몇 줄만 보고 "아, 방향이 틀렸네"라고 판단해 중단할 수 있다는 점이다. 기다림이 짧아지는 게 아니라, 기다릴지 말지를 사용자가 결정할 수 있게 된다.
세 개의 시간을 따로 본다
- 첫 토큰까지 — 사용자가 "반응했다"고 느끼는 시점. 여기가 가장 중요하고, 여기서만 스켈레톤이나 진행 표시가 의미를 갖는다.
- 토큰 사이 간격 — 끊기면 멈춘 것처럼 보인다. 네트워크 버퍼링이나 프록시 설정 때문에 청크가 뭉쳐 오는 경우가 흔하다.
- 완료까지 — 길이를 예측할 수 없으므로 진행률 바(0~100%)는 대체로 거짓말이 된다. 차라리 표시하지 않는 편이 낫다.
스트리밍에서 실제로 걸려 넘어지는 지점
구현 자체는 어렵지 않다. 서버 라우트에서 응답을 흘려보내고 클라이언트에서 청크를 받아 붙이면 된다. 문제는 그다음에 생긴다.
모델은 마크다운을 자주 쓴다. 그런데 코드 펜스가 ``` 하나만 열린 상태, 표가 헤더까지만 온 상태에서 HTML로 변환하면 화면이 우스꽝스럽게 깨진다. 해법은 둘 중 하나다. 스트리밍 중에는 평문으로 흘리고 완료 시 한 번 렌더하거나, 미완성 구문을 견디는 증분 파서를 쓰는 것이다. 후자가 좋아 보이지만 직접 구현하면 예외 케이스가 끝없이 나온다.
텍스트가 늘어날 때마다 맨 아래로 스크롤하면, 위쪽을 다시 읽으려던 사용자를 계속 끌어내린다. 사용자가 스크롤을 올린 순간 자동 추적을 끄고, 대신 "맨 아래로" 버튼을 띄우는 방식이 안전하다.
답변 영역이 0px에서 시작해 늘어나면 아래 요소가 계속 밀린다. 컨테이너에 최소 높이를 주거나 영역을 미리 확보해 두자. 스트리밍은 본질적으로 누적 레이아웃 이동을 유발하기 쉬운 패턴이다.
실패를 전제로 설계하기
일반 API 호출은 성공 아니면 실패다. LLM 호출에는 그 사이가 있다. 절반쯤 오다 끊긴 응답, 사용자가 일부러 멈춘 응답, 형식이 어긋난 응답이 전부 다르게 처리돼야 한다.
| 상황 | 사용자에게 보여야 할 것 | 피해야 할 것 |
|---|---|---|
| 사용자가 중단 | 거기까지 온 내용은 남기고, 이어쓰기 또는 다시 시도 선택지 | 화면을 지워버리기 — 사용자는 멈춘 것이지 취소한 게 아니다 |
| 중간에 연결 끊김 | 받은 데까지 표시 + 끊겼다는 사실을 명시 | 부분 응답을 완성본처럼 보여주기 |
| 속도 제한(429) | 잠시 후 재시도 안내, 가능하면 남은 대기 시간 | 즉시 자동 재시도 반복 — 상황을 악화시킨다 |
| 거부·안전 필터 | 왜 응답할 수 없는지 담백하게 | 일반 오류로 뭉뚱그리기 |
| 형식 위반(JSON 깨짐) | 사용자에게는 재시도, 로그에는 원문 보존 | 파싱 실패를 조용히 삼키기 |
중단 기능은 선택이 아니다. 브라우저 표준 AbortController로 요청을 끊는 것 자체는 몇 줄이면 되지만, 중단 시점까지의 텍스트를 어떻게 다룰지는 제품 결정이다. 대화형이라면 남기는 편이, 단발성 생성이라면 지우는 편이 자연스럽다.
const controller = new AbortController();
// 요청
const res = await fetch("/api/generate", {
method: "POST",
body: JSON.stringify({ prompt }),
signal: controller.signal,
});
// 중단 버튼
stopButton.onclick = () => controller.abort();주의할 점은 중단해도 서버 쪽 호출이 자동으로 멈추지는 않는다는 것이다. 연결만 끊길 뿐 모델은 계속 생성하고 있을 수 있고, 그만큼 비용도 발생한다. 서버 핸들러에서 클라이언트 연결 종료를 감지해 상위 호출까지 취소하도록 연결해 두어야 실제로 절약된다.
비용이 화면 설계를 바꾼다
프론트엔드 개발자가 청구서를 신경 써야 하는 상황은 흔치 않았다. AI 기능은 다르다. 화면에서 내린 결정이 그대로 토큰 사용량이 되고, 토큰은 곧 돈이다. 비용에 영향을 주는 UI 결정은 대체로 세 군데다.
- 얼마나 많은 맥락을 함께 보내는가 — 대화 기록을 통째로 매번 붙이면 대화가 길어질수록 요청 하나가 비싸진다. 오래된 턴을 요약하거나 잘라내는 정책이 필요하고, 그 판단은 UI가 대화를 어떻게 다루는지에 달려 있다.
- 같은 요청이 몇 번 나가는가 — 버튼 연타, 실수로 인한 재제출, 자동 재시도. 전송 중 버튼 비활성화와 짧은 디바운스만으로 상당 부분이 막힌다.
- 어떤 모델을 쓰는가 — 요약이나 분류처럼 가벼운 작업까지 최상위 모델로 돌릴 이유는 없다. 작업 종류에 따라 모델을 나누는 것은 서버 로직이지만, 그 분기의 기준은 대개 사용자가 어떤 UI를 눌렀는지다.
중단 버튼을 다시 보자. 사용자 입장에서는 편의 기능이지만 운영 입장에서는 비용 통제 장치다. 답이 산으로 가는 걸 보면서도 끝까지 기다리게 만드는 UI는 사용자 시간과 토큰을 동시에 태운다.
같은 질문이 반복되는 화면(예: 문서 요약, 정형화된 추천)이라면 결과를 캐시해 두는 것만으로 호출량이 크게 준다. 서버 캐시가 정석이지만, 세션 안에서 동일 입력을 다시 보내지 않는 정도는 클라이언트에서도 충분히 막을 수 있다.
보안 — 키, 프롬프트 주입, 출력 렌더링
API 키는 브라우저에 두지 않는다
가장 흔하고 가장 비싼 실수다. 번들에 넣든, 빌드 타임 환경변수로 주입하든, 브라우저까지 내려간 값은 개발자 도구 몇 번이면 드러난다. 모델 호출은 반드시 서버(라우트 핸들러·엣지 함수·백엔드)를 거치게 한다. 그 서버에서 사용자 인증, 사용량 제한, 로깅을 함께 처리하면 자연스럽게 남용 방어선이 된다.
사용자 입력이 지시문이 된다
프롬프트 주입은 서버 쪽 문제로 여겨지지만, 입력 경로를 만드는 건 프론트엔드다. 사용자가 직접 친 문장뿐 아니라 업로드한 파일, 붙여넣은 웹페이지, 불러온 외부 데이터가 전부 모델에게는 "읽어야 할 지시"처럼 보일 수 있다. 특히 외부 콘텐츠를 요약하는 기능은 그 콘텐츠 안에 심어둔 문장이 시스템 지시를 덮어쓰려 시도할 수 있다는 전제로 다뤄야 한다.
프론트엔드에서 할 수 있는 최소한은 이 정도다. 입력 길이 제한, 파일 형식·크기 제한, 외부에서 가져온 콘텐츠를 사용자 입력과 시각적으로 구분해 표시하기. 그리고 모델이 위험한 동작을 하기 전에는 사용자 확인을 받는 것이다. 메일 발송이나 결제처럼 되돌리기 어려운 행동을 모델 판단만으로 실행하게 두면 안 된다.
모델 출력은 신뢰할 수 없는 입력이다
이 문장 하나가 렌더링 파트의 전부다. 모델이 만든 마크다운을 HTML로 바꿔 그대로 삽입하면 전형적인 XSS 경로가 열린다. 새니타이저를 반드시 거치고, 링크는 대상과 rel 속성을 통제하고, 이미지 출처를 제한한다. 굳이 서식이 필요 없는 자리라면 일반 텍스트로 렌더링하는 게 가장 안전하다.
| 위험 | 프론트엔드에서 할 일 |
|---|---|
| 키 유출 | 클라이언트에서 직접 호출 금지, 서버 경유 + 인증·쿼터 |
| 프롬프트 주입 | 입력·파일 제한, 외부 콘텐츠 구분 표시, 위험 행동 전 사용자 확인 |
| 출력 XSS | 마크다운 변환 시 새니타이즈, 링크·이미지 출처 통제 |
| 민감정보 노출 | 사용자가 무엇을 전송하는지 화면에 명시, 첨부 미리보기 제공 |
접근성 — 스트리밍과 스크린 리더
스트리밍 UI에서 가장 자주 망가지는 게 접근성이다. 원인은 대개 선의에서 출발한다. 응답 영역에 라이브 리전을 걸어두는 것이다.
문제는 토큰이 들어올 때마다 내용이 바뀐다는 점이다. 스크린 리더가 매 갱신을 읽으려 들면 같은 문장을 계속 다시 읽거나, 읽는 도중 끊기기를 반복한다. 눈으로 볼 때는 자연스러운 화면이 소리로는 소음이 된다.
- 스트리밍 영역 자체는 조용히 둔다. 생성 중임을
aria-busy로 표시하되, 본문이 갱신될 때마다 낭독하게 만들지 않는다. - 상태 변화만 알린다. "답변 생성 중", "답변 완료" 같은 짧은 문구를 별도의 정중한(polite) 라이브 영역으로 한 번씩 전달한다.
- 완료 후 본문에 접근할 수 있게 한다. 다 끝난 텍스트를 처음부터 읽을 수 있으면 충분하다. 실시간 낭독이 목표가 아니다.
- 중단 버튼에 명확한 레이블을 준다. 아이콘만 있는 정지 버튼은 스크린 리더에서 정체를 알 수 없다.
- 키보드만으로 돌아가는지 확인한다. 전송·중단·재시도·복사는 전부 탭 이동과 엔터로 접근돼야 한다.
<!-- 본문: 조용히, 상태만 별도로 -->
<div id="answer" aria-busy="true">...</div>
<p class="sr-only" aria-live="polite">답변을 생성하고 있습니다</p>접근성 전반의 점검 항목은 웹 접근성 실전 체크리스트에서 따로 다뤘다. AI 기능은 그 위에 "계속 변하는 콘텐츠"라는 변수 하나가 더 붙은 경우라고 보면 된다.
2026년 도구 지형
직접 다 만들 필요는 없다. 다만 도구가 UX를 대신 설계해 주지는 않는다는 점만 기억하면 된다.
- Vercel AI SDK — 사실상 표준에 가까운 위치다. 서버 쪽 생성·도구 호출을 다루는 코어와,
useChat·useCompletion같은 프레임워크 훅을 제공해 스트림 상태 관리를 대신해 준다. 프로바이더를 한 줄로 교체할 수 있다는 점이 실무에서 특히 유용하다. 2026년에는 중단·재개가 가능한 장기 실행 작업(Workflows)까지 생태계가 넓어졌다. - 프레임워크 — React는 19.2 계열, Next.js는 16.2 계열이 현행이다. 서버 컴포넌트와 라우트 핸들러가 자리를 잡으면서 "키는 서버에, 스트림은 클라이언트로"라는 구조를 짜기가 예전보다 훨씬 수월해졌다.
- SDK 없이도 된다 — 표준
fetch와ReadableStream만으로 충분히 구현된다. 의존성을 늘리기 싫거나, 스트림 처리 방식을 직접 통제해야 한다면 이쪽이 낫다.
어떤 SDK를 쓰든 중단하면 무엇이 남는지, 실패하면 무엇을 보여줄지, 맥락을 어디까지 보낼지는 직접 정해야 한다. 이 세 가지를 정하지 않은 채 훅부터 붙이면, 데모는 30분 만에 되지만 그 뒤로 몇 주가 걸린다.
착수 전 체크리스트
AI 기능을 화면에 올리기 전에 답을 정해두면 좋은 항목들이다. 코드가 아니라 결정 사항이다.
- 모델 호출이 서버를 경유하는가. 클라이언트 번들 어디에도 키가 없는가
- 첫 글자가 나오기까지 화면에 무엇이 보이는가
- 사용자가 중단할 수 있는가. 중단하면 지금까지의 내용은 남는가
- 서버 쪽 호출도 함께 취소되는가 (연결만 끊고 과금은 계속되고 있지 않은가)
- 연결이 끊겼을 때 부분 응답임을 알 수 있는가
- 속도 제한과 거부 응답이 서로 다른 메시지로 안내되는가
- 대화 맥락을 어디까지 잘라 보내는가
- 전송 중 버튼이 잠기는가. 연타로 중복 요청이 나가지 않는가
- 모델 출력을 HTML로 그릴 때 새니타이즈를 거치는가
- 스크린 리더에서 소음이 되지 않는가. 키보드만으로 전 과정이 되는가
① 프론트엔드 AI 활용은 코드 생성과 기능 탑재 두 갈래이며, 어려운 쪽은 후자다.
② 스트리밍은 총 시간이 아니라 첫 글자 시점과 중단 선택권을 준다.
③ 성공·실패 사이에 부분 응답이라는 제3의 상태가 있다.
④ 맥락 길이·중복 요청·모델 선택은 전부 UI가 만드는 비용이다.
⑤ 키는 서버에, 출력은 신뢰할 수 없는 입력으로 취급한다.
⑥ 스트리밍 본문은 조용히, 상태 변화만 알린다.
정리하면 이렇다. AI 기능을 붙인다고 해서 새로운 프론트엔드 기술이 필요한 건 아니다. 다만 지금까지 당연하게 여겼던 전제 몇 개가 무너질 뿐이다. 응답은 즉시 오지 않고, 길이를 모르고, 중간에 끊기며, 출력할 때마다 값을 치른다. 그 네 가지를 UI 어디에서 받아낼지 먼저 정하고 시작하면, 나머지는 늘 하던 일이 된다.
자주 묻는 질문
프론트엔드에서 AI를 활용한다는 게 정확히 무슨 뜻인가요?
두 갈래로 나뉩니다. 하나는 AI로 코드를 짜는 것(개발 생산성)이고, 다른 하나는 AI 기능이 들어간 화면을 만드는 것(제품 구현)입니다. 앞쪽은 도구를 고르는 문제라 비교적 단순하지만, 뒤쪽은 응답이 느리고 길이를 예측할 수 없으며 실패할 수 있다는 전제 위에서 UI를 다시 설계해야 하는 문제입니다.
AI 응답을 왜 스트리밍으로 보여줘야 하나요?
완성된 답을 기다리면 수 초에서 수십 초의 빈 화면이 생깁니다. 스트리밍은 총 소요 시간을 줄이지 못하지만 첫 글자가 보이는 시점을 앞당겨 체감 대기를 크게 줄입니다. 사용자가 초반 몇 줄만 보고 방향이 틀렸다고 판단해 중단할 수 있다는 점도 중요합니다.
API 키를 프론트엔드에 두면 안 되는 이유는?
브라우저로 내려간 코드와 네트워크 요청은 누구나 열어볼 수 있습니다. 번들에 넣든 환경변수로 주입하든 결과는 같습니다. 호출은 반드시 서버(라우트 핸들러·엣지 함수)를 거치게 하고, 그 서버에서 인증·사용량 제한·로깅을 함께 처리해야 합니다.
AI가 만든 텍스트를 화면에 그릴 때 주의할 점은?
모델 출력은 신뢰할 수 없는 입력으로 취급해야 합니다. 마크다운을 HTML로 변환해 그대로 삽입하면 스크립트나 위험한 링크가 섞여 들어올 수 있습니다. 새니타이저를 거치고, 링크는 대상과 rel 속성을 통제하며, 원문 그대로 보여도 되는 곳에서는 일반 텍스트로 렌더링하는 편이 안전합니다.
스트리밍 텍스트는 스크린 리더에서 어떻게 처리하나요?
토큰이 들어올 때마다 읽어주면 소음이 됩니다. 스트리밍 영역 자체는 조용히 두고, 생성 시작과 완료 같은 상태 변화만 별도 라이브 영역으로 알린 뒤 완성된 본문을 한 번에 읽을 수 있게 하는 방식이 낫습니다. 중단 버튼에는 명확한 레이블이 필요합니다.
비용은 프론트엔드와 무슨 상관인가요?
토큰이 곧 비용이라 화면 설계가 청구서를 바꿉니다. 대화 기록을 무한정 붙여 보내는지, 사용자가 실수로 같은 요청을 연타할 수 있는지, 가벼운 작업에도 최상위 모델을 쓰는지가 전부 UI 결정입니다. 중단 버튼, 입력 길이 제한, 재요청 억제는 UX인 동시에 비용 통제 장치입니다.