| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 메일자동화
- php
- sqlite
- next.js
- contextBridge
- REST API
- 배포
- 프론트엔드
- 인증
- python
- Vite
- React
- 크롤링
- 소셜로그인
- FastAPI
- electron
- 마이그레이션
- typescript
- 상태관리
- 아키텍처
- 도메인설계
- Kotlin
- 보안
- Android
- javascript
- 백엔드
- 온디바이스AI
- github actions
- JWT
- playwright
- Today
- Total
HM소프트
로컬 AI 로고 생성 도구 개발기 (1) — CPU만으로 이미지 생성이 가능한가 본문

CPU 디퓨전 추론은 멀티스텝·OpenVINO·INT8 양자화로는 스텝당 10초 벽을 못 넘고, 1-step 증류 모델에서야 장당 80초가 3.7초로 자릿수가 바뀐다. 다섯 번의 스파이크로 측정한 기록과, 티처-스튜던트 증류 구조로 간 이유, 모델이 없어도 끝까지 도는 16px·1색 유용성 게이트와 SVG 벡터화까지의 모델-독립 코어를 정리한다.
코드 한 줄 전에, 가정부터 깨러 갔다
로고·심볼 시안을 완전히 로컬(오프라인)에서 뽑는 개인용 도구를 만들기로 했다. 요구는 두 줄로 요약된다. (1) GPU 없는 평범한 CPU 노트북에서 돌 것, (2) 결과물이 흔한 "AI가 만든 템플릿 로고" 냄새를 풍기면 안 될 것. 인터넷·API 없이 로컬에서 수 초 안에 쓸 만한 시안이 나와야 한다는 뜻이다.
이 두 줄은 서로 잡아당긴다. 품질을 올리려면 큰 모델을 써야 하고, 큰 모델은 CPU에서 느리다. 빠르게 하려고 모델을 줄이면 결과물이 다시 템플릿으로 수렴한다. 그래서 본격 구현에 들어가기 전에 다섯 번의 스파이크(throwaway 실험)와 데이터셋 조사로 이 긴장의 정체부터 측정했다. "되겠지" 하고 짠 코드는 대부분 중간에 뒤집힌다. 그럴 바엔 버릴 셈치고 먼저 재 보는 게 빠르다.
이번 1편은 그 측정 기록이다. CPU에서 디퓨전 모델이 실제로 몇 초가 걸리는지, 어떤 가속 기법이 벽을 못 넘었는지, 왜 결국 "티처-스튜던트 증류"라는 구조로 갔는지, 그리고 모델이 아직 없는 상태에서도 전체 흐름이 돌아가도록 먼저 만들어 둔 모델-독립 로컬 코어까지 순서대로 풀어 본다.
전체 그림 — 학습은 클라우드에서 한 번, 추론은 로컬에서 영원히
먼저 결론부터 그림으로 박아 두자. 다섯 번의 스파이크가 가리킨 종착지는 이 구조였다.

핵심은 시간축의 분리다. 무겁고 느린 작업(고품질 데이터 생성 + 모델 증류)은 클라우드 GPU에서 딱 한 번 치르고, 그 산출물(빠른 1-step 모델)을 로컬에 동봉해 추론은 영원히 오프라인으로 돌린다. 학습 비용은 일회성이고, 사용 비용은 0이다. 이 구조에 도달하기까지의 과정이 이 글의 본론이다.
1단계 질문 — CPU에서 디퓨전이 실용적인 속도로 도는가
가장 먼저 부딪힌 건 단순한 질문이었다. "GPU 없이 디퓨전 모델이 사람이 기다릴 만한 속도로 돌긴 하나?" 감으로 답하지 않고 같은 머신에서 경로별로 직접 시간을 쟀다.
첫 스파이크는 정공법이었다. 범용 베이스 디퓨전 모델(SD1.5급)에 LCM-LoRA를 얹어 스텝 수를 6으로 줄이고, CPU 스레드를 모두 쓰게 한 뒤 5개 브리프 × 4개 프리셋 = 20장을 뽑아 봤다.

guidance_scale=1.5라는 낮은 값이 눈에 띌 텐데, LCM 계열은 가이던스를 낮게 줘야 제대로 동작한다. 그렇게 6스텝까지 줄였는데도 결과는 장당 약 80초였다. 스텝당 거의 10초. 이건 "최적화하면 되겠지" 수준의 격차가 아니다. 인터랙티브 도구에서 한 장에 80초면, 4장을 뽑아 비교하는 데만 5분이 넘는다. 도구로서 죽은 속도다.
두 번째 스파이크에서는 더 깨끗한 베이스 모델로 바꾸고, 디코딩을 거의 즉시 끝내는 초경량 VAE(TAESD)를 붙이고, 프롬프트 적중을 위해 스텝을 8로 올려 봤다. 결과는 오히려 장당 약 91초. 베이스 모델을 잘 고르면 배경 품질 같은 건 좋아졌지만, 속도의 자릿수는 꿈쩍도 하지 않았다. 여기서 분명해진 게 있었다. 속도 문제는 "어떤 모델이냐"가 아니라 "몇 스텝이냐"에 걸려 있다. 스텝당 10초가 상수처럼 버티고 있으니, 스텝 수를 못 줄이면 게임 끝이다.
2단계 — 가속 라이브러리는 벽을 못 넘었다
스텝당 10초의 벽을 깨 줄 후보가 두 개 있었다. CPU 추론 가속 런타임(OpenVINO)과 INT8 양자화. 둘 다 "CPU에서 몇 배 빨라진다"는 벤치마크가 널려 있어 기대를 걸 만했다. 그래서 속도 전용 스파이크를 따로 팠다 — 품질은 다른 스파이크의 일이고, 여기서는 오직 스텝당 시간(s/step)만 본다.
# spike3 — OpenVINO IR 변환 후 스텝당 시간 측정
from optimum.intel import OVStableDiffusionPipeline
model = "stable-diffusion-v1-5/stable-diffusion-v1-5"
pipe = OVStableDiffusionPipeline.from_pretrained(model, export=True, compile=False)
pipe.reshape(batch_size=1, height=512, width=512, num_images_per_prompt=1)
pipe.compile()
for steps in (4, 8):
for label in ("warm", "timed"): # 첫 패스는 워밍, 둘째 패스만 계측
t = time.time()
img = pipe(PROMPT, num_inference_steps=steps, height=512, width=512,
guidance_scale=2.0).images[0]
print(f" {steps} steps [{label}]: {time.time()-t:.1f}s")
계측에서 두 가지를 일부러 분리했다. export(IR 변환)는 일회성 고정비라 따로 재고, 추론은 워밍 패스 다음의 두 번째 패스만 계측했다. 첫 패스에는 컴파일·캐시 비용이 섞여 들어가 진짜 처리량을 가린다.
결과는 실망스러웠다. IR 변환에만 4분 넘게 걸렸고, 정작 추론은 스텝당 11~14초로 오히려 PyTorch보다 느렸다. FP32 OpenVINO는 이 작업에서 가속이 사실상 0이었던 셈이다.
마지막 카드로 INT8 양자화를 꺼냈다. 이건 가중치만 줄이는 weight-only가 아니라, 활성값까지 줄이는 하이브리드 양자화를 몇 개 로고 프롬프트로 캘리브레이션해서 시도했다. 실패하면 weight-only로 폴백하는 구조였다.
# spike4 — 하이브리드 INT8 양자화 (실패 시 weight-only 폴백)
CALIB = [ # 활성값 양자화를 보정할 대표 프롬프트 몇 개
"flat vector logo of a coffee bean in a wind swirl, white background",
"monoline logo of a wave, single line, white background",
# ... (로고 도메인 프롬프트 10여 개)
]
try:
q = OVWeightQuantizationConfig(bits=8, dataset=CALIB, num_samples=len(CALIB))
pipe = OVStableDiffusionPipeline.from_pretrained(MODEL, export=True,
quantization_config=q)
pipe.reshape(batch_size=1, height=512, width=512, num_images_per_prompt=1)
pipe.compile()
time_pipe(pipe, "hybrid")
except Exception as e:
print(f" hybrid path failed: {repr(e)[:300]}") # → weight-only로 폴백
이쪽은 더 험했다. 캘리브레이션에만 25분이 걸렸고, Windows 환경에서 인코딩(cp949) 문제로 도중에 크래시까지 났다. 우여곡절 끝에 weight-only 경로로 떨어졌지만, 그건 메모리는 줄여도 지연 시간(latency)에는 영향이 없었다. 활성값 양자화로 latency를 줄이는 길은 환경 취약성에 막혀 실용적이지 않았다.
여기서 솔직한 결론을 적어 뒀다. 설령 INT8이 2배를 줘서 스텝당 5초가 된다 해도, 4스텝이면 한 장 20초 — 여전히 인터랙티브 큐레이션 루프엔 못 들어간다. 게다가 양자화는 품질까지 깎는다. 상수배 개선으로는 이 문제를 못 푼다는 게 두 번째 단계의 교훈이었다.
3단계 — 스텝 수 자체를 1로 줄이는 증류 모델
상수배가 아니라 자릿수를 바꾸려면 곱셈의 횟수 자체를 줄여야 한다. 스텝당 10초를 못 깬다면, 스텝을 6도 4도 아닌 1로 만들면 된다. 그게 1-step 증류 모델이다. 큰 모델의 멀티스텝 결과를 한 스텝으로 흉내 내도록 미리 증류(distill)해 둔 구조라, 추론 시 디노이징을 단 한 번만 돈다.
세 번째 스파이크에서 이 가설을 세 갈래로 동시에 검증했다. 속도(증류 아키텍처)와 품질(로고 특화)을 따로 떼어 봐야 원인이 섞이지 않기 때문이다.

arm_A의 steps=1, guidance=0.0이 이 도구의 운명을 바꾼 두 숫자다. 1-step 증류 모델은 가이던스도 0으로 둔다(증류 과정에 이미 녹아 있다). 결과는 장당 약 3.7초. 80초에서 3.7초 — 자릿수가 바뀌었다. 속도 문제는 여기서 닫혔다.

하지만 arm_B와 arm_C가 동시에 품질의 함정을 드러냈다. arm_B(베이스 + 기성 로고 LoRA)는 품질은 나아졌지만 어떤 브리프를 넣어도 똑같은 배지(badge) 화풍으로 수렴했고 속도는 179초로 폭증했다. arm_C(꿈의 조합)는 아예 차원이 안 맞아 로드조차 안 됐다 — 1-step 증류 모델은 구조가 특수해서 기성 LoRA를 그냥 못 얹는다. 속도는 증류 모델로 풀렸지만, 품질은 기성 부품으로는 못 푼다는 게 명확해졌다.
4단계 — 품질은 왜 "커스텀 모델"일 수밖에 없었나
품질의 벽은 두 방향에서 왔다.
- 범용 베이스 모델: 결과물이 죄다 비슷한 톤으로 균질하다. "AI가 만든 로고" 특유의 무난하고 밋밋한 인상.
- 기성 로고 LoRA: 반대로 하나의 화풍(보통 둥근 배지/엠블럼)으로 수렴한다. 브리프가 달라도 같은 집에서 찍어낸 듯한 결과가 나온다. 위
arm_B가 정확히 이 실패였다.
그렇다면 직접 데이터를 모아 학습하면 되지 않나? 여기서 세 번째 벽이 나왔다. 데이터셋 조사 결과, 상업적으로 쓸 수 있고 라이선스가 깨끗하면서 디자이너급 품질인 로고 "생성용" 데이터셋이 사실상 없었다. 규모가 큰 셋은 실제 상표를 긁어모은 것이라 라이선스가 불명확하고, 상표권 문제까지 안고 있었다.
이 세 가지 사실 — (1) 속도는 1-step 증류로만 풀리고, (2) 품질은 커스텀 모델이 필요하며, (3) 쓸 만한 기성 데이터셋이 없다 — 이 모이자 길은 하나로 좁혀졌다. 티처가 데이터를 만들고, 그 데이터로 스튜던트를 증류한다.
여기에 사용자(나) 측의 결정 하나가 결정적이었다. 결과물은 개인용·비납품이라 비상업(NC) 라이선스를 허용할 수 있었다. 이 한 가지 덕분에 비상업 라이선스의 강력한 대형 모델을 티처로 쓸 수 있게 됐다. 즉 티처(대형·고품질·NC)로 가공의 브랜드명과 컨셉을 주고 합성 로고 코퍼스를 만든다. 가공 브랜드를 쓰는 이유는 명확하다 — 실제 상표를 학습시키지 않기 위함이다. 그 깨끗한 합성 코퍼스로 빠른 1-step 스튜던트를 증류하면, 품질은 티처에서 물려받고 속도는 증류 구조에서 얻는다.
이 "티처가 디자이너급인지"를 진짜 코드와 비용으로 검증하기 전에, 공짜로 먼저 엿보는 스파이크도 하나 넣어 뒀다. 후보 티처 LoRA들의 공개 샘플 이미지를 받아 한 장에 모아, GPU를 한 푼도 쓰기 전에 "증류해서 살릴 가치가 있는 품질인가"를 눈으로 판정하는 것이다.
# flux_samples — GPU 비용 0으로 티처 품질 먼저 엿보기
REPOS = ["...FLUX.1-dev-LoRA-Logo-Design",
"...Logo-Design-Flux-LoRA",
"...LogoLoraForSDXL-V2"]
for repo in REPOS:
pics = [f for f in api.list_repo_files(repo)
if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))]
for f in pics[:4]: # 생성이 아니라, 제작자 공개 샘플 다운로드
p = hf_hub_download(repo, f)
im = Image.open(p).convert("RGB"); im.thumbnail((300, 300))
imgs.append(im)
# 한 장의 컨택트 시트로 묶어 "디자이너급인가"를 눈으로 판정
가장 위험하고 비싼 가정(티처 품질)을 가장 싸게 먼저 찔러 보는 것 — 스파이크의 정석이다.
모델이 없는데 어떻게 도구를 만드나 — 플러그형 엔진
여기서 현실적인 문제. 스튜던트 모델 학습은 클라우드 GPU 비용과 시간이 드는 일이라 뒤로 미뤄야 했다. 그렇다고 모델이 나올 때까지 앱을 손 놓고 기다릴 순 없다. 그래서 모델을 갈아끼울 수 있는 로컬 코어를 먼저 완성했다. 지금은 임시로 1-step 증류 모델(spike5에서 통과한 그 모델)을 끼워 전체 흐름을 돌리고, 나중에 우리 스튜던트가 나오면 상수 한 줄만 바꾸면 된다.

설계에서 신경 쓴 세 가지. 첫째, INTERIM_MODEL 상수 하나로 모델 교체점을 봉인했다. 스튜던트가 나와도 다운스트림(유용성 게이트·벡터화·UI)은 한 글자도 안 바뀐다. 둘째, 지연 로드(lazy load) — 패키지를 import하는 것만으로 수 GB 모델을 메모리에 올리지 않는다. 셋째, warm()으로 미리 한 번 버리는 추론을 돌려, 사용자의 첫 클릭이 모델 로딩 지연을 뒤집어쓰지 않게 했다. CPU 도구에서 "첫 인상"의 체감 속도는 이 워밍 한 줄이 좌우한다.
생성 자체는 의외로 단순하다. 스타일 프리셋에서 프롬프트를 만들고, 시드를 바꿔 가며 steps=1로 n장을 돌리고, 각 장의 소요 시간까지 같이 기록해 둔다(도구가 자기 속도를 화면에 보여줘야 신뢰가 생긴다).
프리셋은 "트렌드 이름"이 아니라 "시각 문법"이다
품질을 코드 레벨에서 떠받치는 첫 장치가 스타일 프리셋이다. 여기서 의도적으로 피한 게 있다. "미니멀", "모던" 같은 트렌드 이름으로 프리셋을 만들지 않았다. 그런 이름은 모델에게 아무 제약도 주지 못해 결국 균질한 결과로 흐른다. 대신 각 프리셋을 시각 문법(visual grammar) — 어떤 도형으로, 어떤 선으로 구성하는가 — 으로 정의하고, 그 프리셋 특유의 실패 모드를 죽이는 네거티브 프롬프트를 따로 붙였다.

세 가지를 더 짚어 둔다. (1) BASE_NEGATIVE에 text, letters, words, typography가 들어 있다 — 글자는 모델에게 그리게 하지 않는다. 디퓨전 모델은 글자를 못 그린다. 워드마크는 나중에 진짜 폰트로 합성하는 별도 단계로 미룬다. 그래서 각 프리셋이 폰트 페어링까지 들고 있고, 그 폰트는 상업 사용이 가능한 OFL 계열로 골라 둬서 사용 규칙이 바뀌어도 살아남는다. (2) 프리셋마다 negative_extra로 자기 실패 모드만 콕 집어 죽인다. 예컨대 바우하우스에는 "둥근 테두리/감싸는 링"을 금지해 배지화를 막는다. (3) 초안에 있던 일부 프리셋은 잘라냈다 — 한 해 유행으로 끝날 클리셰이거나(트렌드 의존), 문법이 아니라 품질 기준에 가까운 것이었기 때문이다.
프롬프트 조립에서 한 가지 더. brief(브랜드가 무엇인가)와 concept(은유/아이디어)을 분리해 받는다. 컨셉이 없으면 모델은 일반적인 도형으로 표류한다 — 이건 첫 스파이크에서 가장 크게 데인 부분이다. "예쁜 심볼"과 "아이디어가 담긴 로고"를 가르는 건 결국 이 컨셉 한 줄이다.
비협상 장치 — 16px와 1색에서 살아남는가
로고는 예쁜 512px 그림이 아니다. 명함에 작게 박혀도 읽혀야 하고, 팩스나 도장처럼 1색으로 뭉개져도 형태가 살아야 진짜 로고다. 그래서 파이프라인에 유용성 게이트를 비협상 단계로 박았다. 모든 후보를 16/32/48px로 줄이고 1색 임계화한 결과를 만들어, "큰 화면에서 예쁜가"가 아니라 "작은 크기에서 살아남는가"로 컬링한다.

여기 박힌 교훈들이 전부 스파이크에서 데인 자국이다. lift_white는 첫 스파이크에서 배경이 완벽한 순백이 아니어서 1색 변환 때 옅은 회색이 소금-후추 노이즈로 터졌던 문제의 해결책이다. 1색 게이트를 통과시키기 전에 먼저 배경을 강제로 순백으로 깐다. legible_small은 "16px 식별성"을 사람 눈 대신 숫자로 잰다 — 작게 줄였다 키워서 잉크 구조가 얼마나 보존되는지의 비율이다. ink_coverage는 잉크가 너무 얇으면(작게 가면 사라짐) 또는 너무 두꺼우면(뭉텅이) 걸러 낸다. 임계값(0.02~0.55, 0.85)은 보수적 출발점이고 데이터로 튜닝할 자리다.
그리고 사람이 직접 판단할 수 있도록 컨택트 스트립을 만든다. [원본 | 순백처리 | 48 | 32 | 16px | 1색 | 1색@32]을 한 줄로 늘어놓아, 선택 그리드에서 예쁜 타일이 아니라 작은 크기·단색 칸을 보고 추리게 한다. 도구가 사용자에게 "옳은 기준으로 보라"고 강제하는 장치다.
마지막 — 깨끗한 SVG로 떨구기
살아남은 시안은 래스터(픽셀)에서 벡터(SVG)로 변환해야 실제로 쓸 수 있다. 그런데 여기서 함정이 하나 있었다. 디퓨전 출력은 미세하게 부드럽고 노이즈가 끼어 있어, 그걸 그대로 트레이싱하면 쓰레기 경로(path)가 수백 개 쏟아진다. 엔지니어링 리뷰에서 짚은 진짜 위험이 이거였다. 해법은 "더 좋은 트레이서"가 아니라 전처리 — 트레이싱 전에 색을 몇 개의 평면 색으로 평탄화하고 배경을 깨끗이 까는 것이다.

핵심은 flatten을 이름 붙은 필수 단계로 승격시키고, dither를 꺼서 평면 영역이 깨끗이 만들어지게 한 것이다. 디더링은 부드러운 그라데이션을 흉내 내려고 점을 뿌리는데, 그게 바로 트레이서를 미치게 하는 노이즈다. 그리고 to_svg가 path 개수를 반환한다는 게 영리한 부분이다. 깨끗한 로고는 수십 개의 path, 쓰레기는 수백 개 — path 수가 곧 품질 신호다. svg_quality()가 그 숫자를 사람이 읽을 수 있는 등급("clean / acceptable / garbage")으로 번역해, 사용자가 "이건 더 평탄화하거나 버려라"를 즉시 알 수 있게 했다.
전부 한 화면에서 — 로컬 Gradio 앱
이 모든 조각(브리프 입력 → 생성 → 유용성 그리드 → 선택 → 벡터화 → SVG 다운로드)을 Gradio UI 하나로 묶었다. 임시 모델로도 지금 당장 로컬에서 끝까지 돈다.
# app.py — 생성→유용성 그리드→선택→벡터화 한 흐름
def do_generate(brief, concept, preset_key, n, seed):
gens = ENGINE.generate(brief, preset_key, concept or None,
n=int(n), base_seed=int(seed))
raws, strips, store = [], [], []
for i, g in enumerate(gens):
rep = usability.assess(g.image) # 유용성 자동 판정
raws.append((g.image, f"#{i} seed={g.seed} {g.seconds:.1f}s"))
strips.append((usability.contact_strip(g.image), # 16px/1색 스트립
f"#{i} usable={rep.passes} ink={rep.ink:.2f} 16px={rep.legible16:.2f}"))
store.append({"path": _save_tmp(g.image), "verdict": ...})
avg = sum(g.seconds for g in gens) / len(gens)
return raws, strips, f"Generated {len(gens)} @ avg {avg:.1f}s/img.", store
def do_vectorize(selected_idx, store, n_colors):
img = Image.open(store[selected_idx]["path"]).convert("RGB")
svg, npaths = vectorize.to_svg(img, n_colors=int(n_colors))
status = f"SVG: {npaths} paths — {vectorize.svg_quality(npaths)}" # path 수 노출
return flat, svg_path, status
UI가 단순히 그림만 보여주는 게 아니라 장당 평균 초와 유용성 점수, SVG path 수까지 화면에 노출한다는 점이 중요하다. 도구가 자기 성능과 품질을 숨기지 않고 사용자에게 보고하면, 사용자는 감이 아니라 숫자로 시안을 고르게 된다. 그리고 이 UI는 나중에 스튜던트 모델로 갈아끼워도 한 줄도 바뀌지 않는다 — 엔진 뒤의 인터페이스가 고정돼 있기 때문이다.
정리하며
- 코드 한 줄 전에 다섯 번 측정했다. "되겠지"로 짠 코드는 뒤집힌다. 가장 위험한 가정(CPU 속도, 티처 품질)을 가장 싸게 먼저 찔렀다.
- CPU 온디바이스 이미지 생성은 1-step 증류 모델로만 실용적(약 80초 → 약 3.7초). 베이스 교체·OpenVINO·INT8은 상수배 개선이라 스텝당 10초 벽을 못 깼다. 자릿수를 바꾸려면 스텝 수 자체를 줄여야 한다.
- 품질은 기성 부품으로 안 된다. 범용 베이스는 균질화, 기성 로고 LoRA는 단일 화풍 수렴. 쓸 만한 데이터셋도 없다. → 티처가 가공 브랜드로 깨끗한 합성 코퍼스를 만들고, 그걸로 빠른 스튜던트를 증류하는 구조로 확정.
- 모델이 없어도 멈추지 않도록 모델-독립 코어를 먼저 완성했다.
INTERIM_MODEL상수 한 줄로 교체점을 봉인, 지연 로드 + 워밍, 시각 문법 프리셋, 16px/1색 유용성 게이트, 평탄화 후 SVG 벡터화까지 임시 모델로 끝까지 돈다. - 프리셋은 트렌드 이름이 아니라 시각 문법으로, 글자는 모델이 아니라 폰트 합성으로, SVG 품질은 path 수로 신호화 — 품질을 코드 레벨 장치로 떠받쳤다.
다음 편은 가장 위험한 가정의 검증, 즉 클라우드 GPU에서 티처가 정말 디자이너급인지를 일회성으로 띄워 보고(비용 통제를 위해 끝나면 즉시 내리는 스팟 VM), 그 합격/불합격을 기준으로 합성 코퍼스 생성과 스튜던트 증류로 넘어가는 과정을 기록할 예정이다.
'기술 개발일지' 카테고리의 다른 글
| AI 에이전트 기반 웹 QA 도구 개발기 (1) — 시니어 QA처럼 움직이는 자율 탐색 테스터 (0) | 2026.06.18 |
|---|---|
| 프로젝트 산출물 자동화 개발기 (1) — 규칙 엔진과 검증 훅을 갖춘 문서 생성 플러그인 (0) | 2026.06.18 |
| AI 코딩 에이전트용 디자인 시스템 플러그인 개발기 (1) — 'AI 티 나는 디자인'을 강제로 깨뜨리기 (0) | 2026.06.15 |
| 실시간 미리보기 + html2canvas 이미지 내보내기 — 텍스트 교체 도구 (3) (0) | 2026.06.12 |
| 스텝 위저드 UX와 전역 상태 설계 — 이미지 텍스트 교체 도구 (2) (0) | 2026.06.12 |