왜 또 생성기인가
Day 3에 만든 카카오톡 대화 생성기의 YouTube판입니다.
콘텐츠 제작자가 블로그나 영상에 "이런 댓글 달렸어요" 같은 예시를 넣을 때, 또는 리뷰 글에 실제 댓글 레이아웃을 보여주고 싶을 때 쓰는 도구입니다. 기존 카톡 생성기에서 잘 통했던 패턴 — useReducer 하나로 상태 통합 + 좌측 설정 패널 + 우측 프리뷰 + html2canvas-pro로 PNG 내보내기 — 을 그대로 가져와서 시작했습니다.

아바타를 3번 갈아엎었다
이게 오늘 시간을 가장 많이 먹은 부분입니다.
v1: 이모지 아바타 (😀 🙂 🎬 …)
처음에는 카톡 생성기처럼 이모지 셀렉터를 넣었습니다. 결과는 "가짜처럼 보이는 가짜". YouTube는 모든 아바타가 실제 사진이라 이모지가 등장하는 순간 위화감이 폭발합니다.
v2: 이니셜 아바타
Gmail/GitHub 스타일로 이름 첫 글자 + 컬러 배경. 한글은 첫 글자 그대로, 영문은 대문자. 훨씬 자연스럽긴 한데, 유튜브를 오래 쓴 사람이라면 "진짜 같은데 좀 허전하네" 수준입니다.
v3: 이미지 업로드
결국 실제 이미지를 업로드할 수 있게 했습니다. 관건은 state 비대화 방지 — data URL을 그대로 저장하면 댓글 10개만 돼도 수 MB가 될 수 있습니다. 그래서 업로드 시 canvas로 256px까지 축소하고 JPEG 0.85로 재인코딩:
async function fileToDataUrl(file: File, maxSize = 256): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const img = new Image();
img.onload = () => {
// 긴 변을 maxSize에 맞춰 비율 유지 축소
const canvas = document.createElement("canvas");
// ...
resolve(canvas.toDataURL("image/jpeg", 0.85));
};
img.src = reader.result as string;
};
reader.readAsDataURL(file);
});
}최종 렌더 우선순위: redacted(모자이크) > image > initial. 이 순서 때문에 나중에 버그 하나가 더 터집니다 (뒤에서).
프사 모자이크를 "진짜처럼" 만들기
댓글 생성기에서 은근히 중요한 기능입니다. SNS 스크린샷을 보면 다른 사람 프사는 거의 항상 모자이크나 블러로 가려져 있습니다. "이 컴포넌트로 실제 캡처처럼 만들 수 있어야 한다"가 기준이었습니다.
솔직히 단색 원이나 CSS filter: blur()도 고려했는데, 전자는 너무 밋밋하고 후자는 html2canvas로 export하면 결과가 예측 불가능합니다. 그래서 SVG 5×5 <rect> 그리드 + 원형 클립으로 직접 그렸습니다.
포인트는 같은 이름은 항상 같은 패턴이 나와야 한다는 것. 렌더할 때마다 색이 바뀌면 장난감처럼 보입니다. 이름을 시드로 FNV-1a 해시를 돌려서 결정적으로 색을 뽑습니다:
function mosaicColors(seed: string, count: number): string[] {
let h = 2166136261;
for (let i = 0; i < seed.length; i++) {
h ^= seed.charCodeAt(i);
h = Math.imul(h, 16777619);
}
// xorshift로 count만큼 팔레트에서 결정적으로 뽑기
}shapeRendering="crispEdges"로 픽셀 경계를 흐리지 않게 유지하면, 진짜 모자이크 처리된 이미지 같은 느낌이 납니다.
In-place 편집 모드
처음엔 모든 수정이 왼쪽 설정 패널의 폼에서 일어났습니다. 그런데 막상 써보니 불편했습니다. "이 댓글의 좋아요 숫자만 바꾸고 싶다"고 하면 리스트에서 해당 항목을 찾아 드롭다운을 고르고… 단계가 너무 많습니다.
그래서 프리뷰 위에 프리뷰 / 편집 탭을 붙이고, 편집 모드에서는 작성자명·시간·본문·좋아요 수를 프리뷰 위에서 직접 클릭해서 수정할 수 있게 했습니다.

구현은 contentEditable + 자체 래퍼 컴포넌트 EditableText. React에서 contentEditable을 쓸 때 가장 골치 아픈 건 외부 value가 바뀔 때마다 textContent를 덮어쓰면 커서가 맨 앞으로 튀는 현상입니다. 해결은 의외로 단순했습니다. 포커스가 있으면 DOM에 손 대지 않기:
useEffect(() => {
const el = ref.current;
if (!el || document.activeElement === el) return; // 포커스 중엔 건드리지 않음
if (el.textContent !== value) el.textContent = value;
}, [value]);편집 모드에만 나오는 플래그 토글 툴바, 삭제 버튼, 편집/프리뷰 탭에는 전부 data-html2canvas-ignore="true"를 달아뒀습니다. 편집 중에 그대로 이미지 저장을 눌러도 export된 PNG에는 툴바가 안 찍힙니다.
편집 UI의 이모지를 전부 걷어냈다
초반엔 플래그 버튼을 📌 ✓ 👑 ❤ 🔒 🗑 이렇게 이모지로 만들었습니다. 하나씩 보면 직관적인데, 6개가 한 줄로 모이니 의미 해독이 오히려 느려졌습니다. Day 8 유튜브 다운로더 리디자인 때 얻은 교훈이 정확히 다시 나왔습니다:
이모지가 많을수록 AI가 만든 티가 나고, 가독성도 떨어진다.
결국 전부 한글 2~4자 라벨로 교체했습니다: 고정 / 인증 / 주인 / 하트 / 프사모자이크 / 삭제 / 프사제거. 더 긴 설명이 필요한 건 title 속성으로 툴팁을 달아뒀습니다 (예: 고정 → "상단 고정 댓글").

그 와중에 찾은 CSS 버그 (진짜 어이없는 것)
편집 모드를 붙이고 나서 화면을 보는데, 답글의 "채널주인 ❤" 하트 오버레이가 아바타 옆이 아니라 아바타에서 한참 떨어진 이상한 위치에 그려졌습니다.
원인을 찾는 데 한 시간 정도 날렸습니다. 결론부터 말하면 이겁니다:
Avatar컴포넌트의.relative래퍼에 크기 지정이 없었다- flex row의 기본값
align-items: stretch때문에 래퍼가 형제인 "본문 + 답글" 컨텐츠 열 높이만큼 stretch - 래퍼 안의
absolute -bottom-0.5오버레이는 stretched된 래퍼의 바닥에 앵커 - 결과: 하트가 아바타에서 수백 px 떨어진 답글 근처에 그려짐
해결은 한 줄:
<div
className="relative shrink-0 self-start"
style={{ width: size, height: size }}
>래퍼에 명시적 width/height를 주고 self-start로 flex stretch를 차단하니 하트가 제자리로 돌아왔습니다.
교훈: absolute positioning의 기준 박스가 "내가 시각적으로 생각하는 것"이 아닐 수 있다. flex container 안의 relative wrapper는 거의 항상 stretch되기 때문에, 크기를 명시하거나 align-self를 걸어주지 않으면 나중에 이런 식으로 배신당합니다.
데이터 싱크 함정
또 하나 재밌었던 버그. 영상 설정의 채널 소유자 이름을 "홍선생"으로 바꿨는데, 고정 댓글의 @채널주인이 그대로였습니다.
원인은 단순했습니다. creatorName(설정)과 comment.author(데이터)가 완전히 분리된 채 각자 렌더되고 있었습니다. 고정 배너의 "고정됨 by XX"는 creatorName을 보고, 댓글 본체의 @XX는 comment.author를 봤던 것.
해결은 reducer의 UPDATE_SETTINGS 케이스에서 creatorName 변경을 감지하면 isCreator: true인 모든 댓글·답글의 author까지 함께 업데이트하도록 cascade 처리했습니다:
case "UPDATE_SETTINGS": {
const next = { ...state, ...action.payload };
if (action.payload.creatorName !== undefined &&
action.payload.creatorName !== state.creatorName) {
const newName = action.payload.creatorName;
next.comments = state.comments.map((c) => ({
...c,
author: c.isCreator ? newName : c.author,
replies: c.replies.map((r) => ({
...r,
author: r.isCreator ? newName : r.author,
})),
}));
}
return next;
}교훈: 단일 소스가 없으면 반드시 싱크 버그가 난다. 두 곳에서 같은 의미의 값을 쓰면, 언젠가 한 곳만 업데이트하는 코드가 끼어들기 마련이고, 그게 사용자 손에서 터집니다.
카톡 생성기로 역수입
유튜브 댓글 생성기에서 오늘 하루 다듬은 패턴들이 그대로 카톡 대화 생성기로 역수입됐습니다. Day 3에 만들어둔 버전은 이모지 아바타 + 좌측 패널 폼 편집만 되는 수준이었는데, 저녁에 같은 원리로 한 번에 업그레이드했습니다.
카톡 생성기에 반영된 것들:
- 이모지 → 실루엣 + 업로드: 카카오톡 기본 프로필을 닮은 회색 실루엣 SVG를 기본값으로, 클릭하면 이미지 업로드
- Redaction (카톡 "대화 내보내기" 스타일): 체크 시 실루엣으로 되돌리고 이름은
상대1,상대2로 번호화 - 상태바 사실감: 배터리 % 실시간 반영 (≤20%는 빨강), iOS/Android 양쪽에 % 텍스트
- 편집 / 프리뷰 탭: 대화방 이름, 메시지 본문·시간, 상태바 시간·배터리, 발신자 이름, 아바타까지 프리뷰 위에서 전부 인라인 편집. 편집 모드 affordance로 dashed outline을 모든 편집 가능 요소(텍스트 + 아바타)에 붙여 "뭐가 편집 가능한지 모르겠다" 문제 제거
- 같은
EditableText컴포넌트: 포커스 중 DOM 덮어쓰기 차단 로직까지 동일
두 생성기가 이제 같은 DX를 공유합니다. 유튜브 쪽에서 버그나 UX 개선이 나오면 카톡 쪽에도 바로 적용할 수 있게 됐고, 반대도 마찬가지입니다.
회고
- 초기 MVP는 5~6시간, 그 뒤 디테일·버그 수정에 4~5시간 더 들어갔습니다. 기능 추가보다 **"진짜처럼 보이는 가짜"**를 만드는 데 더 많은 시간이 드는 건 Day 3 카톡 생성기 때와 똑같은 패턴입니다.
- 아바타 한 개 디자인에 3번 뒤집은 게 좀 과했나 싶지만, 결과적으로는 업로드 + 이니셜 + 모자이크 3단 구조가 "실제 스크린샷과 구분이 안 되는 수준"까지 올라왔습니다.
9일차 교훈: 이모지 많이 쓴 UI는 예쁘지 않다. 그리고 absolute 쓸 때는 부모 크기를 항상 의심하자.
직접 써보기: /youtube-fakecomment