2026. 7. 1. 11:05ㆍ개발로그
일렉트론 기반의 스크립트와 연동된 에디터 페이지 작업중, 편집을 하다가 화면이 멈추는 현상이 있었다. 사실은 올게 왔다, 라고 생각했다. 회사에서는 ai agent 로 최대한 빨리빨리 진행하기를 원하고, 프론트엔드, 백엔드 할 것 없이 한 프로젝트에 투입되며 규칙은 지켜지지 않고 코드가 꼬이는것은 한순간이었다. 스파게티 그 자체..
가장 큰 문제는 Context 이다. 모든 상태를 Context 에 넣고 앱 전체를 감싸고 있으니, 거의 대부분의 컴포넌트가 스크립트 한 줄 변경, 리전 드래그 동작 하나마다 모두 리렌더되는 상황.. 이전에 한번 거대 리팩토링을 거쳤지만 또 이런문제가 발생했다는것은 팀전체의 리렌더와 상태관리 이해도 문제라고 생각했다. 이 문제를 풀고, 앞으로도 예방하기위해서 어떤것을 할 수 있을까?
Zustand, useState 등, 적당한 도구를 적절한 기준으로 나누어서 적용하는것은 당연히 할 일이고, 왜 이렇게 설계가 되었는지, Context를 다룰 때 왜 조심해야하는지에 대한 내용을 전달하기로 마음먹었다.
1. 왜 리렌더를 먼저 의심했는가
React에서 리렌더(render phase)는 컴포넌트 함수가 다시 실행되어 새로운 UI 트리를 계산하는 과정을 말한다. 여기서 중요한 점은 리렌더 자체가 항상 문제는 아니라는 것이다.
문제가 되는 경우는 다음과 같다.
- 변경된 상태와 무관한 컴포넌트까지 다시 계산되는 경우
- 리스트나 타임라인처럼 요소 수가 많은 영역이 고빈도로 다시 렌더링되는 경우
- 렌더링 중 무거운 계산이 함께 실행되는 경우
- 상태 변경이 연쇄적으로 다른 상태 변경을 유발하는 경우
이 프로젝트에서는 타임라인과 SRT panel이 동시에 넓은 범위로 다시 렌더링되고 있었다. 따라서 키 입력 지연은 원인이라기보다 과도한 렌더링으로 인해 관찰된 증상에 가까웠다.
2. 무엇을 확인했는가
확인해보니 placement라는 메타데이터가 리전에 상속되어 있었다. 문제는 이 값이 projectContext 안에 들어 있었다는 점이다.
리전 위치를 움직일 때마다 placement 관련 상태가 바뀌었고, 그 결과 projectContext를 구독하는 컴포넌트들이 함께 리렌더 대상이 되었다. 타임라인뿐 아니라 SRT panel 안의 고빈도 변경 정보도 같은 projectContext를 타고 있었다.
정리하면 확인한 사실은 이렇다.
- 리전 위치 변경 시
projectContext값이 변경되었다. projectContext를 타임라인과 SRT panel의 넓은 영역이 사용하고 있었다.- 고빈도 변경 상태와 프로젝트 저장 상태가 같은 경계 안에 섞여 있었다.
- 그 결과 실제 변경과 무관한 컴포넌트까지 리렌더 대상이 되었다.
여기서 “원인”을 엄밀히 나누면, 1차 원인은 큰 Context 하나가 앱 전역의 상태를 함께 전달하고 있었다는 구조다. 더 본질적인 원인은 팀 안에서 React state, Context, Zustand store의 역할과 선택 기준이 충분히 정리되어 있지 않았다는 점이다.
3. store는 무엇인가?
store는 상태의 수명과 변경 전파 범위를 관리하는 경계다.
즉 store를 나누는 이유는 단순히 파일을 분리하기 위해서가 아니다. store는 다음 질문에 답해야 한다.
- 이 상태를 얼마나 오래 보존해야 하는가
- 이 상태가 바뀔 때 어디까지 다시 계산되거나 렌더링되어야 하는가
4. store를 나누는 기준
상태를 나눌 때 기준은 두 가지였다.
- 프로젝트 저장 시 필요한 상태인가, 아니면 프로젝트 사용 중에만 필요한 상태인가
- 이 상태가 변경될 때 함께 다시 계산되거나 렌더링되어야 하는 범위는 어디까지인가
이 기준으로 나누면 저장해야 하는 값과 저장하면 안 되는 값이 섞이지 않는다. dirty 상태나 autosave 기준도 명확해진다. 또한 고빈도 상태 변경이 앱 전체로 전파되는 것을 줄일 수 있다.
물론 단점도 있다. store 수가 늘어나고, cross-store update를 조심해야 한다. “이 상태는 어디에 둬야 하는가?”라는 팀 규칙도 필요하다. 그래서 구조 변경만 하고 끝내면 같은 문제가 다시 생길 가능성이 높았다.
5. 상태 관리 기준 공유하기
이 문제를 해결하면서 팀 전체에 useState, Context, Zustand, Vanilla Zustand, Class의 용도를 다시 공유했다.
간단히 정리하면 다음과 같다.
| 방식 | 적합한 용도 | 특징 |
|---|---|---|
useState |
가까운 UI 상태 | 가장 단순하고 지역적이다 |
Context |
거의 바뀌지 않는 값 전달 | 기본적으로 selector가 없다 |
Zustand |
여러 React 컴포넌트가 공유하는 고빈도 상태 | selector로 필요한 값만 구독할 수 있다 |
| Vanilla Zustand | React 밖에서도 필요한 상태 | getState, setState, subscribe를 사용할 수 있다 |
| Class | 상태보다 규칙과 lifecycle이 중요한 객체 | method 중심으로 동작을 캡슐화한다 |
특히 Context는 store라기보다 “멀리 있는 컴포넌트에 값을 주입하는 수단”에 가깝다. theme, locale, authUser, apiClient, storeInstance처럼 거의 바뀌지 않는 값에는 잘 맞는다.
반대로 timeline row, region placement, selected track, zoom level처럼 자주 바뀌고 일부 컴포넌트만 알아도 되는 값은 Context에 직접 넣기 어렵다. 기본 Context는 value 단위로 구독되기 때문에, 일부 값만 바뀌어도 해당 Context를 읽는 컴포넌트들이 다시 렌더링 대상이 된다.
Zustand는 selector를 통해 필요한 값만 구독할 수 있다.
const selectedTrackId = useEditorStore(state => state.selectedTrackId);
이 차이 때문에 자주 바뀌는 timeline 상태나 SRT panel 상태는 Context보다 Zustand가 더 적합했다.
5. 결론
이번 문제는 단순한 렌더링 최적화 문제가 아니었다. 더 정확히는 상태의 수명과 변경 전파 범위를 정의하지 않은 구조에서 발생한 성능 문제였다.
Context 하나에 프로젝트 저장 상태, UI 임시 상태, 고빈도 편집 상태가 함께 들어가면 변경의 영향 범위를 예측하기 어렵다. 작은 리전 이동이 타임라인 전체와 SRT panel 전체를 다시 계산하게 만들 수 있다.
그래서 해결의 핵심은 “Context를 Zustand로 바꿨다”가 아니다.
상태가 어디까지 살아야 하고, 어디까지 변경을 전파해야 하는지 먼저 정한 뒤 store 경계를 나누는 것이 핵심이었다.
이후 이상적인 store 구조를 제안하고 팀과 논의했다. 그리고 같은 문제가 반복되지 않도록 관련 폴더 안에 상태 관리 규칙을 문서화했다. 사람이 읽기 위한 문서이면서, 동시에 자동화된 코드 작성 도구가 프로젝트 규칙을 해석할 수 있도록 명시적인 기준을 남겼다.
'개발로그' 카테고리의 다른 글
| Electron에서 여러 창의 상태를 하나의 진실로 맞추기 (0) | 2026.06.13 |
|---|---|
| [드래그 최적화 시도 3] 오버레이 방식으로 드래그 리렌더 제거하기 (0) | 2026.06.10 |
| [드래그 최적화 시도 2] 회고록: 정말 최선이었을까? (0) | 2026.06.10 |
| [드래그 최적화 시도 1] React 경로를 줄이고 한 프레임에 한 번만 그리기 (0) | 2026.06.09 |
| [썸네일 생성 최적화 시도 3] Web Worker를 다시 의심하고 검증하기 (0) | 2026.06.09 |