느려터진 에디터 좀 고쳐줘를 AI에게 시켜봤다
Auto Research 기법을 활용한 LLM 기반 자율 성능 개선 시스템 구축 여정
Polar • AX · Forward Deployed Engineer
- Frontend
1년전
1년이나 미뤄둔 숙제가 있었어요. 채널톡 위지윅 에디터에 큰 문서를 띄우면 입력이 끊긴다는 이슈가 있었어요. 원인까지는 이미 파악된 상태였구요.
NodeView(NodePortal)가 문제였어요. 한 노드만 바뀌어도 연결된 NodeView들이 전부 같이 리렌더링되는 구조라, 표나 코드블록이 많은 문서일수록 더 느려졌어요.
곧 개선 작업이 시작될 예정이었는데, 팀 이동이 겹치면서 위지윅 담당자가 사라졌고, 그렇게 1년이 흘렀어요
AI한테 알아서 시키면 안 되나?
그 1년 사이에 AI 코딩 환경이 정말 많이 바뀌었잖아요.사람이 직접 하나씩 최적화하지 않아도, AI가 스스로 성능 개선을 반복하게 만들 수 있지 않을까 생각했어요. 그러다 발견한 게 Auto Research 였어요.
Auto Research, 그게 뭐예요?
올해 3월, Andrej Karpathy가 공개한 오픈소스인데요. 한 줄로 정리하면 이래요.
"점수가 오르는 변경은 keep, 안 오르면 git revert."
이걸 무한히 돌리는 루프예요. 구조도 단순해요. 파일이 딱 세 개거든요.
prepare.py— 평가 코드. 사람이 정하고 AI는 못 건드려요train.py— AI가 마음대로 고칠 수 있는 샌드박스program.md— 사람이 적어주는 연구 방향과 제약
AI는:
train.py를 수정하고커밋하고
5분간 학습 돌리고
점수를 측정한 뒤
좋아졌으면 다음 가설로, 나빠졌으면 revert
이걸 계속 반복해요.
Ralph Loop랑 뭐가 달라요?
처음엔 저도 Ralph Loop 랑 많이 헷갈렸어요. 둘 다 "AI가 알아서 무한 루프를 돈다"는 점에서는 비슷해 보이거든요.
Ralph Loop는 "완수형" 루프예요
PRD(요구사항 문서)와 TODO 리스트를 두고, 다 끝낼 때까지 반복해요
매 iteration은 깨끗한 컨텍스트로 시작하고, 기억은
progress.txt나AGENTS.md같은 파일에 누적돼요"여기 적힌 거 다 했어요"가 되면 끝나요. 종료 조건이 명확해요
Auto Research는 "탐색형" 루프예요
끝이 정해져 있지 않아요. 점수가 오를 수 있는 동안 계속 돌아요
매 iteration마다 가설을 세우고 → 실험하고 → 점수가 올랐는지 검증 해요
점수가 안 오르면 revert. 그래서 잘못된 방향으로 코드가 무너지지 않아요
위지윅은 Auto Research랑 특히 잘 맞는 문제였어요.
단순히 “AI가 코드 수정하기 좋은 영역” 정도가 아니라,실험 → 측정 → 개선 → 검증 루프를 자동화하기에 조건이 정말 잘 갖춰져 있었거든요.
점수표를 만들기 쉬워요
렌더링 카운트, 벤치마크, 단위 테스트처럼 React 생태계엔 정량 지표로 바꾸기 좋은 도구가 이미 많았어요. 무엇보다 “빨라졌는가?”를 숫자로 판단할 수 있다는 게 컸어요
개선할 엔진이 명확해요
NodeView / Portal 같은 경계가 비교적 잘 나뉘어 있어서, AI가 탐색해야 할 코드 범위도 자연스럽게 좁혀졌어요.
실험 결과를 계속 누적하기 좋아요성능 개선은 한 번에 정답이 나오기보다, 여러 가설을 던지고 비교하면서 조금씩 방향을 찾아가는 경우가 많잖아요. 위지윅은 benchmark 결과나 렌더링 패턴 변화가 잘 드러나서, 어떤 시도가 실제로 의미 있었는지 계속 축적하며 탐색하기 좋았어요
회귀 안전망이 풍부해요기존 테스트가 꽤 탄탄하게 갖춰져 있어서, “성능은 좋아졌는데 기능이 깨졌다” 같은 문제를 빠르게 잡아낼 수 있었어요
1. 하루 만에 만든 PoC
처음 만든 루프는 진짜 단순했어요.
기존 테스트들을 점수표로 두고 "이 점수 올려봐"라고 시켰어요.
어? 코드가 점점 개선되어 보이는 거예요. 너무 신기해서 얼른 개선된 코드를 살펴보고 테스트도 진행했어요.
하지만 막상 PR 열고 코드 리뷰를 해보니, 실제 성능이 좋아진 건 아니었어요
점수표상으론 분명 좋아졌는데, 실제로 위지윅 띄우고 큰 문서에서 타이핑해보면 체감은 거의 똑같았어요. 문제는 다른 데 있었던 거예요.
2. 점수표를 다시 만들기
원인을 뜯어보니 결국 점수표 정확도 문제였어요.
기존 테스트는 기능을 검증하려고 만든 거지, 사용자가 느끼는 성능을 측정하려고 만든 게 아니거든요. 둘은 같은 코드 위에서 돌아도 전혀 다른 곡선을 그려요.
그래서 환경을 다시 짰어요.
React Profiler 기반 렌더링 지표를 직접 만들었어요. NodeView가 몇 번 렌더링되는지, 어떤 노드가 불필요하게 같이 깨지는지 정확히 잡으려구요
환경에 대한 변수를 통제하려고 벤치마크 전용 위지윅 에디터 페이지를 따로 팠어요. 다른 컴포넌트 노이즈를 최소화 하기 위함이였어요
실제 병목 시나리오 그대로 점수에 반영했어요
큰 문서 로드
표(NodeView) 안에서 타이핑
코드블록 옆에서 타이핑
이렇게까지 환경을 다시 짜고 나서야 "점수가 오른다 = 실제 체감 성능이 좋아진다" 가 어느 정도 맞아떨어지기 시작했어요.
3. 출근 루프의 탄생
그런데 또 다른 데서 문제가 터졌어요.
Auto Research는 루프를 꽤 많이 돌려야 흐름이 보여요. 한두 번으론 가설-검증 사이클이 충분히 쌓이지 않더라구요.
경험상 20번쯤은 돌아야 이 방향이 진짜 잘 설계 되었는지 파악할 수 있었어요. 그런데 한 번 도는 데 시간이 꽤 걸려요.
빌드 → 벤치마크 페이지 띄우기 → 시나리오 실행 → 측정 → 코드 수정 → 다시 측정 -> ...이걸 루프가 끝날때까지 앉아서 지켜보는 건 현실적으로 불가능했어요. 그래서 자연스럽게 이렇게 됐어요.
…출근 루프가 탄생했어요
돌리다 보니 점수표 부족한 부분이 계속 보였고, 그걸 보완하는 작업이 의외로 오래 걸렸어요.
결국 "AI한테 맡기면 끝!" 같은 그림은 절대 아니더라구요.
오히려 점점 또렷해졌던 건 실제 개선에 필요한 점수표가 의미 있는 점수를 매기고 있는지, 벤치마크 테스트가 적절한 지표를 보고 있는지가 중요했어요
결과 정리
50번 루프를 돌렸고, 그 중 5개가 머지됐어요.
결과 | 개수 | 비고 |
|---|---|---|
Keep (머지됨) | 5 | 점수 올라간 변경 |
Discard | 31 | 점수가 안 올랐거나 다른 지표 깨짐 |
Crash | 14 | 빌드/실행 자체 실패 |
벤치 지표 변화 (5K 블록 문서)
루프 점수의 베이스가 되는 지표들이에요.
측정 항목 | Baseline | After | Δ |
|---|---|---|---|
Loop score (composite) | 50.0 | 54.2 | +8.4% |
User INP | 104ms | 80ms | -23% |
Long task total | 53ms | 0ms | 완전히 사라짐 |
Long task max | 53ms | 0ms | 사라짐 |
Headless edit latency | 16ms | 14ms | -12% |
Headed render time (NodeView edit) | 13.3ms | 12.2ms | -8.3% |
NodeView edit renders | 3,290 | 3,290 | flat |
Scroll FPS | 60 | 60 | flat |
Memory | 117MB | 117MB | flat |
커밋별 변화 추적
머지된 5개 커밋이 점수에 어떻게 기여했는지요.
#: baseline
Commit: -
변경 요지: -
Loop Score: 50.0
User INP: 104ms
Long task: 53ms
────────────────────────────────────────
#: 1
Commit: 8412c468
변경 요지: NodeView update에 reference equality 체크 + portal을 React.memo로 감싸기
Loop Score: 52.5 (+5.0%)
User INP: 80ms
Long task: 0ms
────────────────────────────────────────
#: 2
Commit: d4ef6bd6
변경 요지: Portal Map → useSyncExternalStore 기반 per-key 구독으로 교체 + decorations 얕은 비교
Loop Score: 53.2 (+1.3%)
User INP: 80ms
Long task: 0ms
────────────────────────────────────────
#: 3
Commit: e4862a03
변경 요지: Leaf NodeView(mention 2,112개) 렌더 skip + Map.get 제거
Loop Score: 53.4 (+0.4%)
User INP: 104ms
Long task: 0ms
────────────────────────────────────────
#: 4
Commit: 13925582
변경 요지: skip 조건을 isLeaf → !isTextblock 으로 확장 (blockquote 714, callout 464개 추가)
Loop Score: 53.5 (+0.2%)
User INP: 96ms
Long task: 0ms
────────────────────────────────────────
#: 5
Commit: 675d4e2c
변경 요지: 이미 포커스 잡혀있으면 view.focus() 스킵 + Array.from → forEach
Loop Score: 54.2 (+1.3%)
User INP: 88ms
Long task: 0ms첫 커밋이 제일 임팩트가 컸어요. “한 노드 바뀌면 3,290개가 같이 리렌더링”되던 구조 자체를 깨버렸거든요.
뒤로 갈수록 디테일을 다듬는 작업이라 점수 상승폭은 줄어들었어요.
Discard된 시도들 — 흥미로운 패턴
23개 discard 중에 패턴이 두 가지 보였어요.
1. 같은 아이디어를 여러 번 시도
useCallback 오버헤드 제거 시도 → 3번
ProseMirror node.eq() 깊은 비교 시도 → 2번
매번 다른 각도로 접근하긴 했는데, 결과는 비슷하게 점수가 미미하게 오르거나 살짝 떨어졌어요. AI가 같은 함정에 또 빠지는 게 보였어요.
2. 점수는 올랐는데 실제로는 회귀
예를 들어 한 시도(2fc5e61b)는 sameDecos 가드를 빼서 모든 non-textblock NodeView가 skip 되도록 했는데, score는 48.4로 떨어졌어요.
분석 로그를 보면:
“the skip logic is not actually preventing renders — the decoration reference change alone is likely forcing reconciliation, adding overhead that inflates edit latency without reducing render count.”
지표만 보면 좋아 보이는 변경이 실제로는 렌더 카운트도 안 줄이면서 latency만 키운 사례예요.
AI 분석 로그가 이걸 잡아냈다는 점은 다행이지만, 반대로 분석 로그가 없었으면 “skip 늘렸네?” 하고 그냥 좋아했겠다 싶더라구요.
결과 분석
좋았던 점
확실히 개선은 됐어요.User INP 104ms → 80ms, long task 53ms → 0ms는 사람 손이 한 번도 안 닿고 나온 결과예요.
사람이라면 망설일 미세 최적화를 잘 시도해요.
Array.from → forEach같이 “이게 진짜 차이 만들까?” 싶은 것도 측정 기반으로 채택/기각이 결정되니까 부담 없이 시도할 수 있어요.시스템을 한 번 만들어두면 재사용돼요.다음에 다른 성능 문제를 잡을 때도 벤치 페이지, 측정 파이프라인, 점수표 골격이 그대로 남아요.
안 좋았던 점
점수표가 너무 유했어요.오차 범위 안의 변경도 통과돼서 큰 의미 없는 변경들이 섞여 들어갔어요.
측정 환경이 매번 미세하게 다르니까 ±2 정도는 사실상 노이즈인데, +0.4를 keep으로 판단한 변경도 있었어요. acceptance threshold를 훨씬 더 빡세게 잡았어야 했어요.
루프가 같은 함정에 반복적으로 빠져요.useCallback 제거, node.eq() 깊은 비교 같은 시도가 살짝 다른 형태로 3~5번씩 반복됐어요.
사람이라면 “아 이건 안 되는 방향이구나” 하고 한 번에 접었을 것 같은데, AI는 비슷한 아이디어를 계속 변주해서 시도하더라구요.
크래시율이 높았어요.14/42가 빌드 실패나 런타임 에러로 죽었어요. 효율이 좋다고 하긴 어려웠어요.
솔직히 이번 케이스는 제가 직접 했으면 더 빨랐을 것 같아요.
1년 묵은 문제라 “AI한테 떠넘기면 더 빠르지 않을까?” 했는데, 벤치 환경 만들고 점수표 다듬고 출근 루프 돌리는 시간까지 합치면 직접 디버깅하는 편이 더 빨랐을 가능성이 커요.
마무리
솔직히 이번 케이스만 놓고 보면, 제가 직접 디버깅하고 최적화했다면 더 빨랐을 가능성이 큰 것 같아요. Auto Research 환경을 만들고, benchmark를 설계하고, score weight를 조정하고, 측정 노이즈를 줄이는 데에도 생각보다 시간이 꽤 많이 들어갔거든요.
그래서 단발성 성능 이슈 하나만 해결하는 목적이었다면, 꼭 가장 효율적인 방법은 아니었던 것 같아요. 다만 흥미로웠던 건, 실제로 시간을 가장 많이 쓰게 된 부분이 “AI가 코드를 얼마나 잘 고치느냐” 자체는 아니었다는 점이었어요.
오히려 더 중요했던 건 평가 환경 설계 쪽이었어요.
benchmark가 실제 체감 성능을 제대로 반영하는지
score가 의미 있는 방향으로 설계되어 있는지
acceptance threshold가 충분히 엄격한지
측정 노이즈를 얼마나 잘 통제하고 있는지
실제로 score가 올랐는데 체감 성능은 그대로인 경우도 있었고, 반대로 미세한 최적화가 long task를 완전히 제거하는 경우도 있었어요. 결국 AI는 주어진 score를 정말 성실하게 optimize할 뿐이더라구요. 문제는 그 score가 우리가 원하는 결과를 제대로 대표하고 있느냐였어요.
그래서 이번 실험은 단순히 “AI로 성능 개선을 해봤다”기보다, AI가 반복적으로 실험할 수 있는 연구 환경을 어떻게 설계해야 하는지에 더 가까운 경험이었던 것 같아요.
특히 benchmark 안정화, score weight tuning, acceptance threshold 같은 부분들을 더 정교하게 다듬는다면, 지금보다 훨씬 더 좋은 결과가 나올 수도 있겠다는 느낌은 분명 있었어요. 어쩌면 앞으로 중요한 건 “더 똑똑한 모델” 자체보다도, AI가 올바른 방향으로 반복 실험할 수 있게 만드는 평가 환경과 연구 루프 설계라고 생각이 들었어요.
관련된 블로그 더보기
처음 써보는 위지윅 에디터 라이브러리에 기여하기
사내 에디터 라이브러리 기여 도전기
Channel.io

