이미지 에디터 개발 기록 - Redo/Undo 기능 추가
모 아니면 도의 에디터
문제
블러 기능을 자체적으로 구현한 것까지는 좋았는데, 문제가 하나 있었다. 이미지에 덧칠하는 작업을 할 때, 작업을 잘못하면 직전 내용을 수정할 수 없이 초기화 버튼을 이용해서 전체 내역을 다 리셋하는 방법 밖에 없었다.
실수하면... 끝이다...
웹에서 간단하게 이미지 작업을 하자고 만든 건데, 오히려 전체 초기화 버튼 하나 밖에 없으니 조금 복잡한 작업을 처리하려 하면 오히려 시간이 더 걸리는 문제가 발생했다.
그래서 웬만한 편집 도구에는 다 있는 Undo/Redo 기능을 추가하기로 했다.
무엇을 스택에 담을 것인가

Undo/Redo 자체는 스택 두 개로 끝나는 표준 패턴이다. 획 히스토리 스택에 작업 내용을 쌓고, Undo 할 때마다 스택에서 하나씩 빼서 Redo 용 스택으로 옮기고, 그 상태에서 새로운 획이 추가되면 Redo 용 스택을 비우는 식이다.
중요한 것은 스택 구조가 아닌, 그 안에 무엇을 담을 것인가였다.
내가 생각한 것은 획의 정보를 담는 것이었다. 각 획이 어디서 시작되어 어디서 끝나는가를 담으면 되지 않을까 싶었다. 클로드도 이 방식에 동의했다.
클로드가 생각했던 방식은 각 단계의 마스크를 저장하는 것이었는데, 마스크 저장 방식의 경우 마스크가 픽셀 당 RGBA 4바이트라서, 스냅샷 한 장이 최대 128MB의 용량을 차지할 수도 있었다. 획 10단계만 거쳐도 1.28GB의 용량을 차지하게 되는 것이다. 이 경우에는 스택에 담는 깊이를 제한하는 방식으로 방어 수단을 설정할 수 밖에 없기에 비효율적이라고 생각했다.
이미지 에디터로 브라우저를 터트리고 싶지 않았기에, 획 히스토리 스택에 획 정보를 담는 것으로 결정했다.
interface Stroke {
points: Point[]; // 붓이 지나간 좌표들
radius: number; // 당시의 브러시 굵기
}
"이 획은 이 점들을 이 굵기로 지나갔다"는 정보만 남기는 방식으로 지정했다. 긴 획 하나가 8KB 남짓이고, 이미지 해상도가 32MP든 2MP든 크기가 항상 같다. 메모리가 이미지 해상도와 완전히 분리된 방식이라 용량 면에서 크게 걱정할 수준이 아니게 처리했다.
Undo와 Redo는 대칭이 아니다
뭐 여기까지는 괜찮았는데, 내가 생각했던 것과 다른 부분이 나왔다. 그것은 Undo와 Redo가 대칭처럼 작용하는 방식이 아니라는 것이었다.
Claude는 Undo와 Redo의 방식에 대해서 Undo는 남아있는 획을 다시 그리는 방식이고, Redo는 그저 획 하나를 추가하는 방식이라고 설명했다.
Claude
마스크는 한 장입니다. 획을 그으면 그 마스크에 계속 덧칠이 되는 구조입니다. 그런데 만약 획 a와 c를 겹쳐 칠했으면, 겹친 픽셀에서 c의 몫만 빼낼 방법이 없습니다. 두 획이 섞인 뒤에는 어느 쪽이 칠한 건지 구분이 사라지기 때문입니다.
그래서 Undo는 마스크를 통째로 비우고 남은 획을 처음부터 다시 재생합니다. a, b, c, d 중 d를 되돌리면 a → b → c 순서로 다시 그리는 것입니다.
반대로 Redo는 더하는 방향이라 지금 마스크 위에 그 획 하나만 얹으면 끝입니다. 전체 재생이 필요 없습니다.
같은 기능처럼 보이지만 비용이 달랐다. Undo는 남은 획 전부를 다시 그리고 Redo는 획 하나만 그리는 방식이었다.
하지만 다행히 이 정도가 이미지 작업에 영향을 주지는 않았다. 20개의 획을 그려서 테스트를 해봤지만 버벅임 없이 Undo 기능이 정상적으로 실행됨을 확인했다. 만약 추후 작업에서 버벅임이 확인된다면 그 때 최적화를 진행하기로 했다.
재생이 정확히 일치하는 조건
현재 구현한 방식은 하나의 전제 조건이 있을 때 성립한다.
바로 블러 강도가 마스크가 아닌 전역 슬라이더에 있다는 것. 마스크는 "어디를 흐리게 할지" 모양만 담기 때문에, 색이나 강도 같은 부가 정보가 없다.
그래서 같은 점들을 같은 순서·같은 짝으로 다시 찍으면 결과 픽셀이 완전히 동일하다. 만약 획마다 블러 강도가 달랐다면 이 전제가 깨지기 때문에 획 정보에 강도까지 기록해야 했을 것이다.
결과
이미지 편집 시 세세한 부분에서 수정이 필요할 경우 간단한 Undo/Redo로 작업을 처리할 수 있어서 편리해졌다. 삐끗한 획 하나 때문에 처음부터 다시 작업을 할 일이 사라졌다.
부수적으로, 여기서 만든 획 히스토리 구조는 나중에 그리기나 텍스트 삽입 기능을 붙일 때 그대로 재사용할 수 있는 형태다. 그때는 획 옆에 다른 종류의 항목을 두고 같은 스택에 넣으면 될 것으로 보인다.
