EDITORCLASSBLOGLOGINimg default descriptionimg default descriptionimg default description표현한다는 것의무한한 가능성,새로운 형태로
담아내다.
img default descriptionimg default descriptionimg default descriptionimg default descriptionimg default descriptionimg default descriptionimg default description새로운 형태의 콘텐츠Opkle은 코드 없이 웹을 마음껏 만들고, 누구나 자기 화면을 그릴 수 있도록 에디터를 만들고, 그 결과물을 어디서나 즐길 수 있게 돕는 팀입니다.텍스트와 화면과의 조화를 통해, 웹을 짓는다는 것이 그저 단순한 코딩이 아닌, 상상을 펼치고 감각을 깨우는 과정이 될 수 있도록 좋은 도구를 만들어 냅니다.
옵클 에디터 개발기: Valkey 큐 서버의 기본 구조dev7옵클 에디터 개발기: Worker loop와 AI 결과 검증dev8옵클 에디터 개발기: 미리보기가 그대로 코드로dev9옵클 에디터 개발기: 그래픽 에디터도 결국 직접 만들기로 했다dev10옵클 에디터 개발기: Rust 공장을 그래픽 엔진으로 다듬다dev117891011...11 옵클 에디터 개발기: Rust 공장을 그래픽 엔진으로 다듬다10편에서 Rust/WASM 공장의 틀을 세웠다면, 그 다음에는 이 공장을 실제 그래픽 에디터가 믿고 호출할 수 있는 엔진으로 다듬어야 했습니다. 단순히 Rust로 빠른 함수 몇 개를 만든다는 감각으로는 부족했습니다. 그래픽 에디터는 사용자가 계속 손으로 만지는 도구입니다. 이미지를 넣고, 색을 보정하고, 흐림을 주고, 브러시로 고치고, 자산을 저장하고, 다시 꺼내고, export까지 이어지는 모든 과정이 하나의 작업 흐름 안에서 움직여야 합니다.그래서 Rust 쪽 코드는 처음부터 의미 단위의 공장처럼 나눴습니다. 흐림과 질감, 색 보정, 래스터 이미지 변환, SVG 렌더링, 픽셀의 벡터화, 브러시 계열 편집, 원근 변환, 프로젝트 자산 압축, 폰트 변환, PDF 렌더링과 페이지 조립처럼 처리 영역을 분리했습니다. 각 영역은 프론트가 직접 구현하기 부담스러운 계산이나 바이너리 처리를 맡습니다. TypeScript는 UI와 document state를 잡고, Rust는 픽셀, 파일, 폰트, PDF, SVG 같은 무거운 작업을 처리합니다.다만 이 구조를 전부 외부에 드러내는 방식으로 만들고 싶지는 않았습니다. 그래픽 엔진의 핵심 설계는 내부에 남겨두고, JavaScript 쪽에는 필요한 API만 열어야 했습니다. 그래서 `wasm_bindgen`으로 공개하는 함수는 명확한 입력과 출력만 갖게 했습니다. 프론트는 RGBA buffer나 Uint8Array, options JSON을 넘기고, Rust는 검증된 Vec이나 Uint8Array를 돌려줍니다. 내부에서 어떤 수치 안정화와 보정 과정을 거치는지는 Rust 공장 안에서 처리됩니다.Rust 공장 지도Rust core의 첫 번째 원칙은 처리 의미를 섞지 않는 것이었습니다. 이미지 필터와 PDF 렌더링, 압축 포맷, 폰트 변환, SVG 래스터라이징을 한 덩어리 안에 몰아넣으면 처음에는 빠르게 만들 수 있어도 나중에 유지하기가 어렵습니다. 그래픽 에디터는 기능이 계속 늘어나는 도구라서, 처리 축을 일찍 나누는 편이 좋았습니다.전체 구조는 거의 지휘판처럼 두었습니다. 바깥에서는 그래픽 엔진의 큰 영역만 보이고, 실제 구현은 각 처리 계층 안에서 분리되어 움직입니다. 한쪽에는 가우시안 블러와 노이즈처럼 커널 기반으로 동작하는 필터 계열이 있고, 다른 한쪽에는 색 보정, 커브, HSL, 샤픈, CMYK preview, bleed mark, layer composite 같은 이미지 편집 계층이 있습니다. 그 옆에는 JPEG, PNG, WebP, SVG 사이를 오가는 포맷 변환 계층과, SVG 문자열을 픽셀 이미지로 바꾸는 렌더링 계층이 붙습니다.벡터 쪽에는 래스터 픽셀을 SVG path로 바꾸는 tracing 계층이 있습니다. binary mode와 color mode를 나누고, 임계값, speckle filter, path precision 같은 옵션을 받습니다. 좌표 변환 쪽에는 이미지 레이어를 4점 기준으로 다시 사영하는 원근 변환 계층이 있고, clone stamp와 healing은 실제 그래픽 에디터의 브러시 계열 도구에 가깝습니다. 프로젝트 자산을 묶고 폰트를 변환하는 계층은 저장, 이동, export를 받쳐주는 하부 구조로 두었습니다.이렇게 나누면 프론트에서도 호출 감각이 분명해집니다. 화면 위의 tool은 TypeScript가 만들지만, 그 tool이 무거운 연산을 해야 할 때는 해당 Rust 처리 계층을 부릅니다. 사용자가 보는 것은 하나의 그래픽 에디터이지만, 내부에서는 이미지 엔진, 벡터 엔진, PDF 엔진, 자산 엔진이 분리되어 움직입니다. 이 분리가 있어야 나중에 기능이 늘어나도 공장 전체가 복잡하게 엉키지 않습니다.WASM 입출력 경계Rust와 TypeScript 사이의 경계도 꽤 신중하게 잡았습니다. 브라우저 쪽에서 Rust를 호출할 때 가장 다루기 좋은 것은 바이트 배열과 문자열입니다. 그래서 많은 함수는 `&[u8]`, `Vec`, `Uint8Array`, `options_json` 같은 형태로 설계했습니다. 복잡한 옵션은 JSON으로 넘기고, Rust 쪽에서 serde로 parse합니다. 이렇게 하면 프론트의 UI state를 그대로 Rust 내부 구조에 강하게 결합하지 않아도 됩니다.픽셀 처리 함수들은 대체로 RGBA buffer를 받습니다. width, height, pixel bytes를 넘기면 Rust는 먼저 길이를 검증하고, 그 다음 RgbaImage나 raw buffer로 처리합니다. 결과도 같은 크기의 RGBA flat bytes로 반환합니다. 이 구조는 canvas ImageData와 잘 맞습니다. TypeScript는 canvas에서 pixels를 가져오고, Rust가 처리한 bytes를 다시 canvas나 blob으로 돌려보낼 수 있습니다.파일 처리 함수는 Uint8Array를 중심으로 잡았습니다. 이미지 변환, SVG 래스터, PDF 조립, 압축/해제, 폰트 변환은 모두 바이너리 입출력입니다. 프론트는 파일을 읽어 Uint8Array로 넘기고, Rust는 포맷을 감지하거나 옵션에 맞춰 변환한 뒤 새 Uint8Array를 돌려줍니다. 이 방식은 브라우저 파일 시스템, IndexedDB, export pipeline과 연결하기 좋습니다.WASM 경계에서 중요한 것은 편한 API보다 흔들리지 않는 API였습니다. Rust 내부 구조를 프론트가 너무 많이 알면 나중에 내부 구현을 바꾸기 어렵습니다. 그래서 공개 함수는 작업 단위로 얇게 두고, 내부 알고리즘과 보정 로직은 처리 계층 안에 숨겼습니다. 프론트는 무엇을 요청할지만 말하고, Rust는 그 작업을 안정적으로 끝내는 구조입니다.수학적으로 보면 이 경계는 대부분 하나의 함수처럼 잡았습니다. 어떤 작업은 `f(buffer, options) -> buffer`이고, 어떤 작업은 `g(bytes, options) -> bytes`입니다. 프론트는 함수의 domain과 codomain만 알고 있으면 됩니다. 내부에서 convolution을 하든, homography를 풀든, thresholding을 하든, PDF object graph를 조립하든, 바깥의 계약은 흔들리지 않습니다. 이 단순한 함수형 경계가 있어야 WASM 엔진이 그래픽 에디터 안에서 안정적인 black box로 존재할 수 있습니다.픽셀 함수이번 Rust 공장은 결국 수학 공장이기도 했습니다. 그래픽 에디터에서 이미지를 조금 흐리게 한다, 밝은 영역만 살린다, 브러시로 특정 부분을 복원한다 같은 말은 UI에서는 간단하게 보이지만, 엔진 안에서는 전부 수학 문제로 바뀝니다. 색상은 벡터이고, 픽셀 버퍼는 격자 위의 함수이고, 필터는 그 함수를 다른 함수로 보내는 연산자입니다.RGBA 이미지는 결국 `I: Z² -> [0, 255]^4` 같은 함수로 볼 수 있습니다. 정수 격자 좌표 `(x, y)`마다 red, green, blue, alpha 네 성분을 가진 벡터가 놓여 있습니다. 어떤 필터는 각 픽셀을 독립적으로 바꿉니다. levels, invert, HSL 보정은 대체로 한 픽셀의 색상 벡터를 다른 색상 벡터로 보내는 pointwise transform에 가깝습니다. 반대로 blur, sharpen, healing, perspective sampling은 주변 픽셀과의 관계를 같이 봅니다. 이때부터는 국소적인 neighborhood와 보간, kernel, 가중 평균이 들어옵니다.이 구분은 코드 구조에도 그대로 영향을 줍니다. pointwise transform은 병렬화하기 쉽고, 메모리 접근 패턴도 단순합니다. convolution이나 bilinear sampling은 주변 픽셀을 계속 참조하기 때문에 cache locality와 boundary condition이 중요합니다. 브러시 도구는 여기에 mask function이 추가됩니다. 사용자의 마우스 위치를 중심으로 반지름 r 안쪽에 radial weight field를 만들고, 그 weight를 alpha composition에 섞습니다.수학을 많이 넣었다고 해서 에디터가 더 복잡해 보이면 안 됩니다. 사용자는 slider와 handle만 만집니다. 대신 Rust 공장 안에서는 좌표, 색상, 확률, 곡선, 행렬, 압축, 페이지 조립이 분명한 경계 안에서 움직여야 합니다. 그래픽 에디터의 완성도는 결국 이 수학 레이어가 얼마나 조용히 안정적으로 돌아가느냐에 달려 있습니다.픽셀 버퍼 검증이미지 작업에서 가장 기본이 되는 것은 픽셀 버퍼 검증이었습니다. 그래픽 에디터에서는 사용자가 어떤 크기의 이미지를 넣을지 알 수 없습니다. 프론트에서 이미 체크했다고 해도 Rust core에서는 다시 확인해야 합니다. width × height × 4를 계산할 때도 u32로 바로 계산하지 않고, u64로 먼저 계산한 뒤 usize 범위를 확인했습니다. 작은 습관처럼 보이지만 WASM 쪽에서는 이런 방어가 중요합니다.픽셀 버퍼 길이가 예상보다 짧으면 바로 에러를 반환합니다. ImageBuffer를 만들 수 없는 경우도 명확한 에러로 돌려줍니다. Rust 공장 안에서는 silent failure를 허용하지 않는 쪽으로 잡았습니다. 그래픽 편집에서 잘못된 픽셀이 조용히 통과하면 나중에 화면 artifact, export 오류, 저장 파일 손상으로 이어질 수 있습니다. 실패할 때는 이른 시점에 실패하는 편이 낫습니다.색상값은 대부분 0에서 255 사이로 clamp합니다. 블러, 노이즈, 커브, HSL, 샤픈, 힐링 브러시 같은 작업은 중간 계산에서 float가 튀기 쉽습니다. 결과를 다시 u8로 내려보낼 때는 반드시 범위를 맞춰야 합니다. 이런 처리를 공통 감각으로 깔아두면 filter가 여러 개 늘어나도 output format이 흔들리지 않습니다.알파 채널도 계속 신경 썼습니다. JPEG는 알파가 없고, PNG와 canvas는 알파가 있습니다. SVG rasterize에서는 premultiplied alpha가 들어오고, PNG로 내보낼 때는 straight alpha로 되돌려야 합니다. 원근 변환이나 bilinear sampling에서도 투명 픽셀의 RGB가 섞이면 검은 테두리가 생길 수 있습니다. 그래서 알파 공간을 어떻게 다루는지가 그래픽 품질에 직접 영향을 줍니다.수학적으로 알파는 단순히 네 번째 채널이 아닙니다. RGB가 실제 색상값인지, alpha가 곱해진 premultiplied vector인지에 따라 연산 결과가 달라집니다. straight alpha에서 `(r, g, b, a)`를 그대로 선형 보간하면 투명 픽셀의 의미 없는 RGB가 색을 오염시킬 수 있습니다. 반대로 premultiplied alpha에서는 `(ar, ag, ab, a)`를 보간한 뒤, 마지막에 a로 나누어 straight space로 되돌립니다. 이 차이는 원근 변환의 가장자리, SVG rasterize의 반투명 경계, 브러시 feather에서 바로 눈에 보입니다.Blur와 noise블러는 그래픽 에디터에서 기본 효과처럼 보이지만, 실제 구현에서는 비용 관리가 중요했습니다. 2D kernel을 그대로 돌리면 픽셀 수와 kernel 크기에 따라 연산량이 빠르게 커집니다. 그래서 가우시안 블러는 separable convolution으로 나눴습니다. 수평 1D pass를 먼저 돌리고, 그 결과를 다시 수직 1D pass로 처리합니다. 2차원 가우시안 `G(x, y)`가 `Gx(x)Gy(y)`로 분해될 수 있다는 성질을 이용하는 방식입니다.kernel은 sigma를 기준으로 만들었습니다. 기본 형태는 `exp(-(x²)/(2σ²))`이고, radius는 대략 3σ 범위로 잡았습니다. 가우시안 분포의 질량 대부분이 그 안에 들어오기 때문에, 무한 support를 가진 연속 함수를 유한한 discrete kernel로 근사할 수 있습니다. 각 weight의 합은 1이 되도록 정규화합니다. 그래야 blur가 이미지를 어둡게 만들거나 밝게 만들지 않고, 전체 에너지를 보존하는 평균 연산처럼 동작합니다.sigma에는 상한도 두었습니다. 사용자가 slider를 끝까지 밀 수도 있고, 외부에서 이상한 값이 들어올 수도 있습니다. NaN, Infinity, 0 이하 값은 방어적으로 처리하고, 너무 큰 sigma는 상한으로 눌렀습니다. 브러시와 필터는 UI에서 값이 온다고 해도 core에서 다시 검증해야 합니다.노이즈는 필름 그레인 감각을 만들기 위해 넣었습니다. 별도의 큰 난수 의존성을 끌어오지 않고 작은 PRNG를 사용했습니다. WASM binary size와 의존성을 생각하면 이런 작은 난수 생성기가 충분했습니다. seed를 받으면 재현 가능한 결과를 만들 수 있고, seed가 0이면 입력 픽셀 일부와 이미지 크기를 기반으로 자동 seed를 만듭니다. 같은 필터라도 매번 미세하게 다른 질감을 만들 수 있습니다.노이즈도 그냥 랜덤 숫자를 더하는 문제가 아닙니다. 픽셀 값이 0이나 255 근처에 있을 때는 더한 값이 clamp되며 분포가 잘립니다. 그래서 결과의 시각적 밀도는 noise amplitude와 clamp 구간의 상호작용을 받습니다. 여기에 blur를 먼저 적용하느냐, noise를 먼저 적용하느냐에 따라 주파수 성분도 달라집니다. blur는 고주파를 깎고, noise는 고주파를 다시 주입합니다. 배경 이미지나 질감 작업에서 blur만 걸면 너무 매끈해지고, noise를 조금 얹으면 시각적으로 자연스러워지는 이유가 여기에 있습니다.이미지 보정 도구이미지 보정 계층은 그래픽 에디터의 filter panel을 염두에 두고 만들었습니다. 노이즈, 노이즈 그라데이션, curve, vibrance, saturation, highlights/shadows, invert, levels, colour balance, HSL, sharpen 같은 축을 나눴습니다. 사용자가 직접 slider를 만질 수도 있고, 나중에는 AI command가 이 값들을 조절할 수도 있습니다.커브는 LUT를 만드는 방식으로 잡았습니다. 채널별 control point를 받아서 256 크기의 lookup table을 만들고, 픽셀마다 해당 table을 적용합니다. 수학적으로는 `[0, 255]`의 이산 정의역 위에 단조에 가까운 transfer function을 만드는 일입니다. control point 사이를 보간해 연속적인 곡선처럼 다루고, 실제 픽셀 처리에서는 그 곡선을 lookup table로 압축합니다. RGB 전체 커브와 채널별 커브를 분리할 수 있게 해두면 사진 보정 감각이 훨씬 넓어집니다.levels는 거의 affine transform에 가깝습니다. input black, input white, gamma, output range를 잡고, 원래의 채널값을 정규화한 뒤 다시 출력 범위로 보냅니다. `x' = ((x - black) / (white - black))^(1/gamma)` 같은 형태의 함수를 생각하면 됩니다. 이 단순한 함수 하나만으로도 어두운 영역을 눌러 contrast를 올리거나, 밝은 영역을 살리는 조절이 가능합니다.색 보정 쪽에서는 luminance와 HSL 공간을 같이 사용했습니다. RGB는 장치 친화적인 좌표계지만, 사람이 밝기, 색상, 채도라고 느끼는 조절과는 약간 어긋납니다. HSL은 완벽한 지각 균등 공간은 아니지만 UI control로는 직관적입니다. hue는 원형 좌표이고, saturation과 lightness는 그 위에 얹힌 축입니다. highlights/shadows는 밝기 영역을 기준으로 조절하고, vibrance와 saturation은 채도 쪽 감각을 다룹니다. CMYK preview와 bleed mark도 EPUB에서 바로 종이책이나 인쇄물 쪽으로 확장될 가능성을 염두에 둔 기능입니다.여기서 중요한 것은 모든 보정 도구를 프론트 UI에 한꺼번에 드러내는 것이 아니었습니다. Rust 공장 안에 안정적인 처리 함수를 먼저 만들어두는 것이었습니다. UI는 나중에 필요한 만큼 꺼내 붙이면 됩니다. core가 준비되어 있으면 그래픽 에디터의 도구 상자는 빠르게 확장될 수 있습니다.브러시와 healing그래픽 에디터에는 단순 필터뿐 아니라 브러시 계열 도구도 필요했습니다. clone stamp와 healing은 그 방향의 시작점이었습니다. stamp는 소스 영역의 픽셀을 target 위치에 브러시 마스크와 함께 복사합니다. 원형 브러시, radius, hardness, opacity를 기준으로 weight를 계산하고, source-over 방식으로 target 위에 올립니다.브러시 마스크는 사실 하나의 radial function입니다. 중심에서의 거리를 d, 반지름을 r이라고 하면 `t = d / r` 같은 정규화 좌표를 만들 수 있습니다. hardness가 높은 브러시는 중심부에서 weight가 오래 1에 가깝게 유지되고, 가장자리에서 급격히 떨어집니다. hardness가 낮은 브러시는 weight가 천천히 감소합니다. 이 감소 곡선에 smoothstep을 쓰면 1차 미분이 튀지 않는 부드러운 feather를 만들 수 있습니다.여기서 중요한 것은 소스와 타겟이 겹칠 때도 결과가 오염되지 않아야 한다는 점이었습니다. 그래서 소스 영역을 원본 스냅샷에서 읽습니다. 순회 중에 out이 바뀌어도 다음 픽셀의 source가 이미 바뀐 결과를 읽지 않게 하기 위해서입니다. 작은 구현 차이지만 clone stamp류 도구에서는 결과의 일관성을 좌우합니다.healing은 stamp보다 조금 더 편집 도구에 가깝습니다. 단순 복사가 아니라 소스 텍스처를 목적지 톤으로 옮기고, 가장자리를 주변과 섞어야 합니다. 내부적으로는 소스 패치와 타겟 패치의 평균 색을 비교하고, 그 차이를 보정 벡터처럼 사용합니다. texture component는 소스에서 가져오되, low-frequency tone은 목적지 주변과 맞추는 감각입니다. 엄밀한 Poisson blending 전체를 그대로 펼치는 방식은 아니지만, 사용자가 기대하는 티 안 나는 복원 쪽으로 가려면 평균, 가중치, 경계 혼합이 모두 필요합니다.alpha composition도 수학적으로는 단순한 덮어쓰기와 다릅니다. source-over 합성은 `C = Cs αs + Cd (1 - αs)` 구조를 갖고, 여기에 브러시 weight와 opacity가 다시 곱해집니다. 결국 한 번의 브러시 stroke는 원본 이미지와 소스 패치 사이의 convex combination입니다. weight가 0이면 원본이 유지되고, 1이면 소스가 강하게 들어옵니다. 이 연속적인 혼합이 있어야 브러시가 픽셀 덩어리가 아니라 실제 도구처럼 느껴집니다.이 단계도 안정적으로 완성되었습니다. Rust 공장은 단순한 WASM 유틸 묶음이 아니라, 그래픽 에디터의 계산 엔진으로 정리되었습니다. WASM 입출력, 픽셀 버퍼 검증, alpha 처리, 필터, 색 보정, 브러시, healing이 하나의 처리 계층으로 맞물렸고, 반복 검수에서도 이미지 처리와 브러시 계열 작업이 안정적으로 통과했습니다. 여기까지 정리하고 나니 다음 단계는 훨씬 분명해졌습니다. 그래픽 엔진의 더 깊은 곳에는 선형대수와 사영기하가 있었고, 그 부분은 별도의 글로 다뤄야 할 만큼 좋은 주제가 되었습니다.이전글목록으로다음글저작권 고시Copyright Notice본 웹사이트의 모든 디자인 결과물 및 영상에 대한 저작권은 Abstract Cloud에 있으며, 저작권법 및 관련 법령에 의해 보호받습니다. 웹, 영상, 본문, 표지, 내지 디자인을 포함한 모든 콘텐츠는 저작권자의 자산으로, 사전 동의 없이 무단 복제, 배포, 2차 저작물 제작, 온라인 공유 등을 금지합니다. 이를 위반할 시, 저작권법에 따라 민형사상 책임을 질 수 있습니다. 정당한 구매와 저작권 보호는 창작자의 권리를 지키며, 더 나은 작품으로 보답할 힘이 됩니다.저작권자: Abstract Cloud | 대표자: 배창규(uragen)
© Abstract Cloud. All Rights Reserved.
HOMEFAQ이용 약관개인정보 이용방침help@opkle.app
010-2747-3403
상호 :추상적 형상 디자인(Abstract cloud)  |  대표자 :배창규사업자등록번호 :249-74-00533통신판매업 신고번호 :2025-의정부송산-0634주소 :경기도 의정부시 부용로 49, 108동 402호웹의 모든 콘텐츠, 디자인, 소스 코드에 대한
저작권은 Opkle에게 있습니다.
img default description