병실 노인을 그렸다 — 글로 그림 그리기를 만든 나흘
글을 쓰고 버튼을 누르면 그림이 나온다. 그게 제대로 되기까지 다섯 번 고쳤다.
글귀를 쓰고 버튼을 누르면 그 글의 그림이 나온다. 마음에 안 들면 다시 그린다. 마음에 들 때만 올린다.
그리기와 게시를 나눈 게 이 도구의 뼈대다. 한 번에 나와서 그대로 올라가면 도구가 아니라 도박이 된다.
나흘 동안 다섯 번 고쳤다. 고친 이유가 매번 달랐다.
한눈에 — 무엇이 어디서 도는가
글귀 하나가 그림이 되어 사이트에 올라가기까지는 이렇게 간다.
관리 화면 → 람다 → (딥시크로 지시문 · Stability로 그림 · S3에 저장 · GitHub에 커밋) → 배포
새로 만든 것은 없다. 사이트를 이미 굴리던 것들에 그림 부르는 호출 하나와 권한 몇 줄을 더했다.
구성도와 쓰는 AWS 리소스
관리 화면 (브라우저)
│
▼ CloudFront /api/* 를 뒤로 넘긴다
│
▼ API Gateway (HTTP API)
│
▼ Lambda nodejs22 · arm64 · 512MB · 제한시간 90초
│ │
│ ├─▶ SSM 파라미터 스토어 비밀값을 읽는다
│ │
│ ├─▶ Bedrock (us-west-2) 딥시크 → 지시문, Stability → 그림
│ │
│ ├─▶ S3 미디어 버킷 style/ 읽기, media/ 쓰기
│ │
│ └─▶ GitHub 글 파일 한 개를 커밋
│
▼ Actions → S3 사이트 버킷 → CloudFront 무효화 → 사이트
Lambda 제한시간이 90초다. 그림 한 장에 수십 초가 걸린다. 일반 API 라면 과한 값인데, 여기는 사람이 버튼을 누르고 기다리는 화면이라 그렇게 잡았다.
arm64를 쓴다. 같은 성능에 더 싸다.
자리마다 왜 그걸 골랐는지.
| 자리 | 고른 것 | 한 번에 | 왜 |
|---|---|---|---|
| 글 읽기 | 딥시크 V3.2 | 약 $0.001 | 그림 모델이 한국어를 잘 못 읽는다 |
| 그림 | Stable Image Style Guide | $0.07 | 기준 그림 한 장으로 화풍을 맞춘다. 이게 기본이다 |
| 그림 (화풍 없이) | Stable Image Core | 더 쌈 | 기준 그림을 안 고를 때만 |
| 지역 | us-west-2 | — | us-east-1 에 쓸 만한 게 없다 |
| 저장 | S3 미디어 버킷 | 거의 0 | 빌드가 만드는 게 아니라 따로 둔다 |
| 게시 | GitHub 커밋 | 0 | 원본이 git 에 남고 기존 배포가 이어진다 |
요금은 2026년 9월 기준이다.
쓰는 AWS 리소스
| 리소스 | 맡은 일 | 어떻게 잠갔나 |
|---|---|---|
| CloudFront | /api/*와 /media/*를 나눠 보낸다 | /api/*는 캐시를 끈다. 매번 새로 처리해야 한다 |
| API Gateway (HTTP API) | 관리 화면의 요청을 받는다 | 경로 구분은 Lambda 안에서 한다 |
| Lambda | 전부 여기서 돈다 | arm64 · 512MB · 90초 |
| Bedrock Runtime | 글 읽기와 그림 그리기 | 부를 수 있는 모델을 넷으로 못박았다 |
| S3 (미디어 버킷) | style/ 기준 그림, media/ 결과 그림 | style/은 읽기만, media/는 쓰기만. 지우는 권한이 없다 |
| SSM 파라미터 스토어 | 비밀번호·GitHub 토큰 | 이 경로 아래만 읽는다. SecureString 이라 KMS 해제 권한도 그 용도로만 |
| CloudWatch Logs | 오류 기록 | 14일만 보관한다 |
세 가지만 덧붙인다.
모델 ARN을 하나하나 적었다. bedrock:InvokeModel을 *로 열어두면, 언젠가 실수로 비싼 모델을 부르는 코드가 그대로 돌아간다. 그래서 쓰는 모델만 주소로 박아뒀다.
화풍 모델은 줄이 둘 필요했다. 추론 프로필로만 부를 수 있는 모델이라 프로필 주소와 원래 모델 주소를 둘 다 열어야 했다.
지우는 권한을 안 줬다. 그림은 올리기만 하고, 기준 그림은 읽기만 한다. 관리 화면이 뚫려도 있던 그림이 사라지지는 않는다.
미디어 버킷은 사이트 버킷과 따로다. 사이트 버킷은 배포할 때 빌드 결과와 똑같이 맞춰지고(안 맞는 건 지워진다), 미디어 버킷은 지우지 않고 버전을 남긴다. 그림은 다시 만들려면 또 돈이 드니까.
1단계 — 붙이기 (8월 22일)
한국어 글을 그림 모델에 그대로 주면 잘 못 알아듣는다.
그래서 중간에 글 읽는 모델을 하나 더 뒀다. 딥시크가 한국어 글을 읽고 영어 지시문을 쓴다. 그 지시문으로 그림을 그린다. 글자는 그리지 말라고 못박아뒀다.
왜 딥시크인가 — 값이 거의 안 든다
단계를 하나 더 두는 건 돈이 더 드는 일이다. 그래서 얼마나 드는지 먼저 봤다.
글 한 번 읽히는 데 0.1센트가 안 든다.
계산
글 하나를 읽히는 데 입력 800 단위, 출력 200 단위쯤 든다. 규칙 문서가 대부분이고 글귀 자체는 짧다.
딥시크 V3.2는 100만 단위에 입력 $0.62, 출력 $1.85다. 곱하면 한 번에 $0.001이 안 된다.
‘단위’는 모델이 글을 세는 토큰을 말한다. 요금은 2026년 9월 기준이다.
그림은 화풍을 맞춰 그리면 한 장에 $0.07이다. 기본이 그쪽이다. 글 읽는 단계는 그림 한 장의 70분의 1쯤이다.
이 비율이 이 구조의 근거다. 그림이 빗나가서 한 번 다시 그리면 $0.07이 그대로 또 나간다. 글 읽는 단계를 넣어서 다시 그리는 일을 한 번만 줄여도 그 단계 값을 70번 치르고도 남는다.
싼 모델을 골라서 아낀 게 아니다. 싼 단계를 앞에 둬서 비싼 단계를 덜 부르게 한 것이다.
성능이 더 좋은 글 모델도 있다. 여기서 하는 일은 짧은 한국어를 읽고 영어 한 문단을 쓰는 것뿐이라, 더 비싼 걸 쓸 이유를 못 찾았다.
여기서 정한 것 둘.
리전을 옮겼다. 그림 모델은 us-west-2에서 부른다. 사이트는 us-east-1에 있는데, 거기엔 글로 그림을 그리는 쓸 만한 모델이 없었다. 편집만 되는 것들이거나, 하나 있는 건 곧 없어질(LEGACY) 모델이었다. 그래서 이 두 호출만 다른 지역으로 나간다.
부를 수 있는 모델을 둘로 묶어뒀다. 열어두면 언젠가 실수로 비싼 모델을 부르는 코드가 그대로 돌아간다. 돈이 새는 건 조용히 샌다.
2단계 — 어느 장면을 그릴 것인가 (8월 23일 밤)
이 글에 그림을 붙였다.
내 나이 85세… 병실 호흡기 없이는 호흡조차 할 수 없는 몸이 되어버렸다. (…)
눈을 감았다 뜨면, 젊은 날로 돌아갈 수 있다면, 좀 더 도전적으로 살아보리라.
그렇게 눈을 뜬 게 오늘이라는 생각으로, 후회없이 살아보자!
병실에 누운 노인이 나왔다.
글이 죽음에서 시작해 오늘로 도착하는데, 그림은 시작점에 서 있었다.
내가 시킨 게 “글의 분위기를 잡아라”였다. 그 글의 지배적 분위기는 병실과 후회다. 시킨 대로 그린 것이다.
문제는 그 글이 병실 이야기가 아니라는 데 있었다. 죽음에서 출발해 “그러니 오늘을 살자”로 도착하는 글이다. 첫 장면은 배경 설명일 뿐이다.
규칙을 바꿨다. 이야기가 있는 글이면 글이 도착하는 장면을 그린다.
같이 고친 것 둘.
- 무엇을 그렸는지 한국어 한 줄로 보여준다. 전에는 알 방법이 없었다. 그림만 보고 “왜 이게 나왔지” 하고 있었다.
- “같은 장면으로 다시”와 “다른 장면으로 다시”를 나눴다. 장면이 마음에 드는데 그림이 아쉬운 것과, 장면부터 틀린 것은 다른 문제다.
덧붙이면, 그 글에 지금 붙어 있는 그림은 여전히 병실이다.
규칙이 안 먹은 건지, 적어 넣어서 고른 건지는 기록에 없다. 다만 도구가 규칙대로만 그리지 않아도 되게 만들어둔 것은 맞다.
3단계 — 내 화풍으로 (8월 23일 밤, 39분 뒤)
규칙을 고쳐도 그림은 계속 사진 같았다.
지시를 세 단계로 세게 밀어봤다. 비싼 모델도 써봤다. 말로는 단순하고 귀여운 쪽으로 안 갔다.
말을 포기하고 그림을 줬다. 기준 그림 한 장을 같이 넘기면 그 화풍으로 그린다.
여기서 중요한 건 이게 학습이 아니라는 것이다. 준비할 데이터도, 돌릴 시간도 없다. 그림 한 장을 같이 보내면 끝이다.
예전에 소설을 자동으로 만화로 바꾸는 걸 만들다 접은 적이 있다. 그때는 내가 그린 그림으로 모델을 학습시키려 했고, 학습에 쓸 이미지를 모으는 것부터 벽이었다. 로컬 GPU로 한 번 돌리면 며칠이 갔다.
그때 벽이던 걸 이번엔 그림 한 장으로 넘었다. 접었던 게 틀렸다기보다 접근이 틀렸던 것이다.
기준 그림은 style/ 아래 둔다. 인터넷에 열려 있는 건 /media/*뿐이라 밖에서는 못 받는다. 직접 그린 그림이고, 이건 재료지 보여줄 것이 아니다.
기본 기준 그림을 정해뒀다. 아무것도 안 골라도 된다.
4단계 — 고르고, 올리고, 이어 다듬고 (8월 24일 낮)
화풍을 그림 보고 고르게. 파일 이름만 늘어놓으면 뭐가 뭔지 모른다. 작은 그림을 같이 내려준다. 고른 건 기억해서 다음에도 쓴다.
올리는 걸 명령 하나로. style-refs/에 파일을 넣는 것만으로는 안 바뀐다. 실제로 쓰는 건 S3 사본이라 맞춰주는 단계가 필요했고, 그걸 손으로 하고 있었다.
node tools/sync-styles.mjs
크기 줄이기, 작은 그림 만들기, 지운 그림 정리까지 한 번에 한다.
그린 그림을 이어서 다듬게. 이게 이번 나흘 중 체감이 제일 컸다.
전에는 고칠 말을 적어도 매번 처음부터 다시 그렸다. “얼굴을 더 크게”라고 적으면 완전히 다른 그림이 나왔다. 고치는 게 아니라 다시 뽑기였다.
이제 앞의 지시문에 고칠 말을 얹는다. 씨앗값을 이어받아 구도를 유지한다. 말하지 않은 부분은 그대로 둔다.
버튼을 셋으로 나눴다.
| 버튼 | 하는 일 |
|---|---|
| 고쳐서 다시 그리기 | 앞의 결과에 고칠 말을 얹는다 |
| 그대로 다시 뽑기 | 지시문은 그대로, 그림만 새로 |
| 처음부터 | 장면부터 다시 고른다 |
같은 날 글귀 화면도 고쳤다. 제목이 본문보다 작아서 제목으로 안 읽혔고, 그림이 제목보다 먼저 나와서 뭘 읽는 건지 알 수 없었다. 제목 → 그림 → 본문 순으로 바꾸고 제목을 키웠다.
5단계 — 미리 적어두는 칸 (8월 25일)
글만 보고 그리니 첫 그림이 빗나가는 일이 잦았다. 빗나가면 고치면 되지만, 고칠 걸 알고 있다면 미리 적는 게 낫다.
‘지시문’ 칸을 하나 뒀다. 그릴 때 지킬 점을 미리 적어둔다.
여기서 조심한 게 하나 있다. 지시문이 장면을 덮어쓰지 않는다. 장면은 그대로 글에서 고르고, 지시문은 그 위에 얹힌다. 줄로 장면을 콕 집어 적었을 때만 그 장면으로 바뀐다. 비워두면 전과 같이 글만 보고 그린다.
덮어쓰게 만들면 편하다. 그런데 그러면 2단계에서 만든 “글이 도착하는 장면을 그린다”가 무력해진다. 매번 손으로 장면을 적게 되고, 그럴 거면 글을 읽힐 이유가 없다.
이 기능을 올리고 12분 뒤에 이 글을 올렸다.
다섯 번 고친 것
| 단계 | 문제 | 고친 것 |
|---|---|---|
| 1 | 한국어를 그림 모델이 못 알아듣는다 | 글 읽는 모델을 중간에 둔다 |
| 2 | 시작 장면을 그린다 | 글이 도착하는 장면을 그린다 |
| 3 | 말로는 화풍이 안 잡힌다 | 기준 그림 한 장을 같이 넘긴다 |
| 4 | 고치려면 처음부터 다시 그린다 | 씨앗값을 이어받아 그 부분만 바꾼다 |
| 5 | 첫 그림이 자주 빗나간다 | 지킬 점을 미리 적어둔다 |
남은 생각
다섯 번 중 셋이 말을 더 잘하는 문제가 아니었다.
2단계는 무엇을 그릴지 정하는 규칙 문제였고, 3단계는 말 대신 그림을 준 것이고, 4단계는 결과를 이어받는 구조 문제였다. 지시문을 아무리 정교하게 써도 이 셋은 안 풀린다.
그리고 셋 다 내가 시킨 대로 나오고 있었다. 병실 노인도 지시대로였고, 사진 같은 그림도 지시대로였고, 매번 새로 그리는 것도 그렇게 만들어놨기 때문이었다.
틀린 게 나오면 모델을 탓하기 쉽다. 그런데 이번 나흘은 전부 내가 시킨 걸 고치는 일이었다.