CodeLog 개발기 (3) 코드 리뷰와 아바타 이미지 변경
코드 토큰화를 통한 라인 선택 기능과 아바타 이미지 캐시 무효화 전략을 다룹니다.
1. 문제의식
CodeLog를 만들면서 마주한 두 가지 문제가 있었습니다.
- 코드 리뷰: 코드의 특정 라인을 선택하고, 그 사이에 댓글 UI를 자연스럽게 끼워넣을 수 있을까?
- 프로필 이미지: 사용자가 아바타를 바꾸면 즉시 반영되게 할 수 있을까?
겉보기엔 전혀 다른 문제지만, 둘 다 "데이터를 어떻게 효과적으로 보여주고 제어할 것인가"라는 본질을 공유합니다.
2. 코드 리뷰: 텍스트를 구조화하기
2-1. 문제 상황
일반적인 <textarea>는 텍스트 덩어리(blob)입니다. 3번째 줄과 5번째 줄 사이에 컴포넌트를 렌더링하려면? 구조적으로 불가능합니다. GitHub처럼 특정 라인을 선택하고 그 자리에 UI를 넣으려면, 텍스트를 제어 가능한 형태로 바꿔야 했습니다.
2-2. 해결: Prism을 통한 토큰화
prism-react-renderer를 사용해 코드를 구조화된 데이터로 변환했습니다.
// "const a = 1;" → Token Array
[
[ { content: "const", types: ["keyword"] }, ... ], // Line 1
[ { content: "a", types: ["variable"] }, ... ] // Line 2
]
이제 코드는 순회 가능한 배열이 되었고, map으로 각 라인을 <div>로 감싸며 원하는 위치에 정확히 UI를 삽입할 수 있게 되었습니다.
2-3. Render Prop 패턴으로 유연성 확보
다음 문제는 재사용성이었습니다. 같은 코드 뷰어를 쓰되, 상세 페이지에선 댓글 폼을, 미리보기에선 읽기 전용 뱃지를 띄워야 했습니다.

export function CodeSnippet({ renderSelectionComponent, ... }: Props) {
return (
<pre>
{tokens.map((line, i) => (
<div key={i}>
<LineContent line={line} />
{isLastSelected && renderSelectionComponent(start, end)}
</div>
))}
</pre>
);
}"어떻게 그릴지"는 CodeSnippet이, "무엇을 그릴지"는 부모가 결정하도록 책임을 분리했습니다. 모바일 환경을 고려해 드래그 대신 클릭 기반 범위 선택도 구현했습니다.
3. 아바타 이미지: 즉각적인 반영
3-1. 문제 상황
프로필 이미지는 자주 바뀌지 않지만, 바뀔 때는 즉시 반영되어야 합니다. 서버 부하는 줄이면서도 사용자가 "빨라졌다"고 느끼게 하는 것이 목표였습니다.
3-2. 해결: 직접 업로드
서버를 거치지 않고 브라우저에서 Supabase Storage로 직접 업로드합니다. 서버 리소스 낭비를 막고 속도를 개선했습니다. Storage Policy(RLS)로 본인 폴더에만 업로드 가능하도록 보안도 챙겼습니다.

3-3. 해결: 캐시 무효화
가장 큰 난관은 캐싱이었습니다. 이미지를 덮어썼는데도 브라우저가 이전 이미지를 계속 보여주는 현상이 발생했습니다.
// 버전 쿼리 스트링으로 강제 갱신
const avatarWithCache = `${publicUrl}?v=${Date.now()}`;
URL 뒤에 타임스탬프를 붙여 브라우저가 새 리소스로 인식하게 만들었습니다.
3-4. 해결: 상태 동기화
DB는 업데이트되었는데 헤더의 아바타는 그대로라면? 전역 상태가 갱신되지 않았기 때문입니다.
// 로컬 상태 먼저 업데이트
setUser({ ...user, avatar: newUrl });
// 이후 서버 동기화
await updateAvatarAction(newUrl);클라이언트 메모리와 서버를 모두 동기화해야 새로고침 없이 즉각 반영됩니다.
4. 배운 것
두 기능은 성격이 다르지만 구현의 디테일이 UX 개선에 영향을 미친다는 교훈을 줍니다.
- 코드를 데이터로 구조화하니 제어 가능해졌습니다.
- 책임을 분리하니 유연하게 재사용할 수 있었습니다.
- 캐시 전략을 세우니 즉각적인 반응이 가능해졌습니다.
사용자가 서비스를 자연스럽게 경험하게 만드는 것이 결국 기술 선택의 목적이었습니다.