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 쪽 코드는 알고리즘 이름보다 계산의 성질에 따라 나눴습니다. 한 픽셀만 읽는 점별 변환, 주변 이웃을 참조하는 국소 연산, 좌표계를 바꾸는 기하 연산, 문서 전체를 읽는 구조 해석은 시간 복잡도와 메모리 접근 방식이 다릅니다. 같은 이미지 처리라는 이유로 한곳에 모으면 임시 버퍼의 수명과 오류 조건이 뒤섞입니다. TypeScript는 상호작용과 문서 상태를 잡고, Rust는 연속 메모리와 수치 계산이 중요한 영역을 맡도록 경계를 정리했습니다.다만 이 구조를 외부 호출부에 그대로 투영하지는 않았습니다. 계산 모듈의 내부 표현과 브라우저의 객체 모델은 변화 속도가 다르기 때문입니다. 경계에서는 메모리의 모양, 좌표의 단위, 색상 표현과 실패 조건만 계약으로 남기고, 내부에서 어떤 자료구조와 최적화를 사용하는지는 감췄습니다. 좋은 FFI는 내부 구현을 자세히 알려주는 창이 아니라 서로 다른 런타임이 오해 없이 데이터를 교환하게 하는 좁은 수학적 계약에 가깝습니다.계산 계층의 분해Rust core의 첫 번째 원칙은 계산 범위를 섞지 않는 것이었습니다. 점별 연산은 입력과 출력의 같은 위치만 대응하므로 병렬화와 타일 분할이 쉽습니다. 합성곱은 반지름만큼의 이웃을 필요로 하므로 타일 가장자리에 halo가 필요합니다. 기하 변환은 출력 픽셀이 원본의 어느 위치를 참조하는지 역함수를 계산해야 하고, 파일 해석은 앞에서 읽은 길이와 버전이 뒤쪽 의미를 결정합니다. 네 종류는 모두 바이트를 다루지만 올바름의 조건이 서로 다릅니다.이 분류는 복잡도 분석에도 직접 연결되었습니다. 너비가 W, 높이가 H인 이미지에서 점별 변환은 대체로 `O(WH)`입니다. 반지름 r인 2차원 커널을 그대로 적용하면 `O(WHr²)`이 되지만 분리 가능한 커널이라면 `O(WHr)`로 낮출 수 있습니다. 벡터화나 문서 파싱은 픽셀 수만으로 비용을 설명할 수 없고 경계의 복잡도, path segment 수와 참조 그래프의 크기가 함께 들어갑니다. 처리량 예측은 기능 이름보다 입력 차원에서 시작해야 했습니다.메모리 관점에서는 읽기 전용 입력과 가변 출력, 재사용 가능한 작업 버퍼를 구분했습니다. 입력과 출력이 같은 버퍼를 공유하면 메모리는 줄지만 아직 읽지 않은 이웃을 먼저 덮어쓸 수 있습니다. 반대로 모든 단계마다 전체 복사본을 만들면 대형 이미지에서 대역폭이 계산보다 더 큰 비용이 됩니다. in-place가 수학적으로 안전한 연산과 별도 버퍼가 필요한 연산을 나누고, 중간 배열의 최대 생존 구간을 짧게 두는 것이 중요했습니다.이렇게 나누면 프론트에서도 호출의 의미가 분명해집니다. 화면 위의 도구는 사용자 의도를 만들고, 계산 계층은 명확한 정의역과 공역을 가진 변환을 수행합니다. 사용자가 보는 것은 하나의 그래픽 에디터이지만 내부 계산은 국소성, 복잡도와 메모리 소유권에 따라 분리됩니다. 이 기준이 있어야 도구가 늘어나도 공장 전체가 하나의 거대한 조건문으로 무너지지 않습니다.WASM 입출력 경계Rust와 TypeScript 사이의 경계도 꽤 신중하게 잡았습니다. 서로 다른 런타임은 객체의 메모리 배치와 문자열 표현, 예외 처리 방식이 다릅니다. 따라서 한쪽의 풍부한 객체를 그대로 건네기보다 차원, stride, channel order와 byte length처럼 해석에 필요한 최소 정보를 명시해야 합니다. 같은 4채널 이미지라도 RGBA인지 BGRA인지, 행 사이에 padding이 있는지, 색상값이 선형광인지 감마가 적용된 값인지에 따라 결과는 완전히 달라집니다.선형 메모리에서는 2차원 이미지도 결국 일렬로 놓입니다. row-major 이미지에서 좌표 `(x, y)`의 채널 시작점은 보통 `base + y × stride + x × channels`로 계산됩니다. 여기서 `stride = width × channels`라고 단정하면 정렬을 위해 행 끝에 여분 바이트가 있는 입력을 잘못 읽을 수 있습니다. 폭과 높이뿐 아니라 실제 행 간격을 계약에 포함시키고, 마지막 접근 위치가 전체 버퍼 길이를 넘지 않는지 큰 정수 공간에서 검증해야 했습니다.메모리 복사 횟수도 API 설계의 일부였습니다. 브라우저 객체를 계산 모듈의 선형 메모리로 옮기고, 결과를 다시 꺼내면 최소 두 번의 대역폭 비용이 발생합니다. 작은 연산을 여러 번 호출하면 같은 이미지가 경계를 반복해서 왕복합니다. 그래서 가능한 경우 일련의 연산을 하나의 계산 그래프로 묶고, 입력이 경계를 건넌 뒤 여러 단계를 마친 다음 최종 결과만 돌려받는 편이 효율적입니다. 함수 수를 줄이는 것이 아니라 데이터 이동 횟수를 줄이는 문제였습니다.WASM 경계에서 중요한 것은 편한 API보다 흔들리지 않는 API였습니다. 내부 자료구조를 호출부가 알기 시작하면 최적화나 알고리즘 교체가 곧 호환성 문제가 됩니다. 경계에는 입력의 단위와 허용 범위, 출력의 소유권, 오류가 발생했을 때 상태가 바뀌었는지만 남겼습니다. 이 계약이 명확하면 계산 내부는 scalar loop에서 SIMD로, 전체 버퍼에서 tile 처리로 바뀌어도 바깥쪽 의미는 유지됩니다.수학적으로 보면 이 경계는 정의역과 공역, 그리고 불변식으로 표현할 수 있습니다. 함수가 받아들일 수 있는 이미지 집합을 D, 정상적인 출력 집합을 C라고 할 때 구현은 단순히 `f: D -> C`만 만족해서는 부족합니다. 알파 범위, 차원, 색 공간과 결정성 같은 불변식도 보존해야 합니다. 잘못된 입력은 D 밖에 있다는 사실을 명확히 알리고 부분 결과를 남기지 않아야 합니다. 이 함수형 경계가 계산 엔진을 안정적인 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를 조금 얹으면 시각적으로 자연스러워지는 이유가 여기에 있습니다.색 공간과 전달 함수이미지 보정은 결국 색을 표현하는 좌표계와 그 위의 함수 문제였습니다. 저장 장치의 RGB 값은 빛의 세기와 선형으로 대응하지 않는 경우가 많습니다. 감마가 적용된 표시값을 그대로 평균하면 물리적인 빛의 평균보다 어두워질 수 있습니다. 블러와 크기 변경처럼 에너지를 섞는 연산은 가능하면 선형광 공간에서 수행하고, 사람이 조절하는 색상과 채도는 목적에 맞는 지각적 좌표계에서 해석해야 했습니다.커브는 유한한 구간에서 정의한 전달 함수입니다. 사용자가 몇 개의 제어점을 정하면 그 사이를 보간해 연속 함수처럼 만들 수 있지만, 제어점 배치에 따라 기울기가 음수가 되거나 overshoot가 생길 수 있습니다. 밝기 순서를 유지해야 하는 보정에서는 단조 보간이 중요하고, 일부러 대비를 뒤집는 효과에서는 단조성 자체가 제약이 됩니다. UI의 작은 곡선 하나에도 함수의 연속성, 미분 가능성과 값의 범위가 들어 있습니다.실제 픽셀 계산에서는 연속 함수를 매번 풀기보다 유한한 lookup table로 표본화할 수 있습니다. 입력 bit depth가 b라면 가능한 채널값은 `2^b`개이고, 각 값의 출력을 미리 계산하면 픽셀당 비용을 거의 한 번의 조회로 줄일 수 있습니다. 다만 더 높은 bit depth나 float 이미지에서는 표 전체가 커지므로 작은 표 사이를 다시 보간하는 편이 낫습니다. 정확도와 cache locality 사이의 균형이 생깁니다.밝기와 색상을 분리하는 좌표계도 하나의 정답은 아니었습니다. HSL은 UI로 설명하기 쉽지만 지각적으로 같은 거리를 보장하지 않고, 선형 RGB는 합성에 유리하지만 사람이 느끼는 채도와 직접 대응하지 않습니다. 인쇄 색 공간은 다시 장치와 프로파일에 의존합니다. 따라서 모든 도구를 하나의 색 공간에 억지로 넣지 않고 연산의 의미에 맞는 공간으로 옮긴 뒤 다시 기준 공간으로 돌아오는 변환 체계를 잡았습니다.여기서 중요한 것은 도구의 수가 아니었습니다. 어떤 공간에서 어떤 함수를 적용했는지, 왕복 변환이 값의 범위와 알파를 보존하는지, 같은 입력이 같은 출력으로 이어지는지를 먼저 확정하는 일이었습니다. 이 수학적 바닥이 준비되면 화면의 조절 방식은 달라져도 계산 결과의 의미는 흔들리지 않습니다.복원 문제와 경계 조건그래픽 복원은 값을 복사하는 문제보다 경계 조건을 어떻게 정하느냐에 가까웠습니다. 어떤 영역 Ω 안의 픽셀을 바꾼다고 할 때, 영역 내부의 질감만 자연스러워서는 충분하지 않습니다. 경계 `∂Ω`에서 주변 이미지와 값이나 기울기가 갑자기 끊기면 사람의 눈은 바로 이음새를 찾습니다. 복원 결과는 내부 정보와 바깥 경계를 동시에 만족해야 합니다.브러시 마스크는 이 경계를 부드럽게 만드는 가중 함수입니다. 중심에서의 거리를 d, 반지름을 r이라고 하면 `t = d/r`로 정규화할 수 있고, weight는 `[0, 1]` 구간의 함수 `w(t)`가 됩니다. 경계에서 값만 0이 되는 것보다 미분값도 0에 가까워지면 합성된 밝기 변화가 더 자연스럽습니다. smoothstep 계열의 함수가 자주 쓰이는 이유도 위치뿐 아니라 기울기의 연속성을 쉽게 확보하기 때문입니다.조금 더 일반적으로 보면 영상 복원은 에너지 최소화 문제로 쓸 수 있습니다. 내부에서는 원본에서 가져온 gradient field를 따르되 경계에서는 대상 이미지와 연결되도록 `min ∫Ω |∇f - v|²` 같은 목적함수를 생각할 수 있습니다. 오일러-라그랑주 방정식으로 옮기면 Poisson 방정식과 연결됩니다. 실제 실시간 도구는 계산 예산에 맞춰 근사할 수 있지만, 무엇을 근사하고 있는지 알면 질감과 저주파 밝기를 왜 따로 다뤄야 하는지가 분명해집니다.합성도 단순한 덮어쓰기가 아닙니다. 전경과 배경이 premultiplied alpha로 표현되어 있다면 source-over는 색과 투명도를 함께 보존하는 결합 법칙을 가집니다. 여기에 공간 가중 함수가 들어가면 한 번의 stroke는 두 영상 사이의 위치별 convex combination이 됩니다. weight의 범위가 벗어나거나 straight alpha와 premultiplied alpha를 섞으면 밝은 테두리와 검은 fringe가 생깁니다.이 단계도 안정적으로 완성되었습니다. Rust 공장은 단순한 WASM 유틸 묶음이 아니라, 복잡도와 메모리 소유권, 색 공간, 경계 조건을 명확히 다루는 그래픽 계산 엔진으로 정리되었습니다. 다양한 크기와 stride의 입력, 투명 경계, 극단적인 보정값과 반복 합성을 검수했고 결과의 결정성과 수치 범위가 모두 안정적으로 유지되는 것을 확인했습니다. 여기까지 정리하고 나니 다음 단계는 훨씬 분명해졌습니다. 그래픽 엔진의 더 깊은 곳에는 선형대수와 사영기하가 있었고, 그 부분은 별도의 글로 다뤄야 할 만큼 좋은 주제가 되었습니다.이전글목록으로다음글저작권 고시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