Mock-up에 집착한 개발자의 WebGL 삽질기

2025-12-01
WebGLShaderMockup


개요

제품 목업을 만든다는 건 단순히 이미지를 합성하는 작업이 아닙니다.
곡면, 주름, 재질, 광원 등 실제 물체가 가진 특성이 그대로 표현되어야 “진짜 같다”는 인상을 만듭니다.
하지만 기존의 목업 제작 환경은 대부분 포토샵 같은 그래픽 도구에 의존합니다.

포토샵 단점

  • 소프트웨어 비용 발생
  • 수작업 기반
  • 반복 작업이 많고, 작업자 역량에 따라 품질 편차가 큼

그래서 저는 브라우저 기반으로 포토샵 없이도 정밀한 목업을 실시간으로 편집할 수 있는 도구를 직접 만들기로 했습니다.
이 글은 그러한 목표 아래 어떤 기술 선택을 했고, 그 과정에서 어떤 시행착오를 겪었는지를 기록한 WebGL 삽질기입니다.

왜 이 프로젝트를 시작 했었는가

포토샵 의존을 벗어난 웹 기반 목업 엔진이 필요했다

제가 이 프로젝트를 시작한 가장 큰 이유는 포토샵이라는 그래픽 툴 의존 자체가 싫었습니다.
특정 소프트웨어를 구매해야 하고, 설치 환경에 따라 제약이 생기고, 툴 사용에 대한 지식이 없으면 만들 시도조차 어려운 한계들이 있습니다.

그래서 저는 목표와 방향성을 정해 보았습니다.

‘포토샵 없이 웹으로 목업을 구현할 수 있는 엔진을 만들자.’

“정말 사실적인 목업”이라는 기준

웹에서 돌아가는 목업 도구는 이미 꽤 존재 했습니다. 하지만 제 기준에선 2% 부족했습니다.

  • 티셔츠 주름을 따라 자연스럽게 눌리는 텍스처
  • 머그컵처럼 곡면이 많은 물체 위에서의 정확한 UV 변형

일반적인 Canvas 합성 수준으로는 절대 만족할 수 없었는데 제가 목표로 한 건 단순히 웹에서 만드는 목업이 아니라 포토샵 없이도 고수준의 목업이었습니다.

개발자로서 ‘만들 수 있는가’를 스스로 확인하고 싶었다

이 프로젝트는 실험이자 도전이었습니다. 평소에 three.js에 대한 가벼운 관심정도와 적용만 해본 수준이었지만 저는 개발자이자 메이커로서 상상한 기술적 결과물을 실제로 구현할 수 있는가에 대한 챌린지이자 누가 대신 만들어놓은 솔루션을 쓰는 게 아니라 기술적으로 처음부터 끝까지 직접 구축해보고 싶었습니다.


Canvas API vs WebGL Shader - 선택의 갈림길

처음에는 Canvas 2D API만으로도 충분할 거라고 생각했습니다.
브라우저만 있으면 어디서든 작동하고, 이미지 합성도 간단하며 drawImage()로 프로토타입을 만드는 데에도 큰 어려움이 없었습니다.

Canvas 2D로 만든 첫 프로토타입

웹 기반으로 만들어 낼 수 있을지에 대한 자체 PoC를 진행해봤습니다.
이미지 텍스처를 불러와서 합성하고, 간단한 투명도 처리를 통해 이 정도면 되겠지 싶은 수준까지는 금방 만들 수 있었습니다.
하지만 이건 저수준의 이미지를 올려놓는 수준의 목업이었습니다.

제가 목표로 했던 사실적인 목업과 비교하면 Canvas는 너무 많은 제약이 있었습니다.

  • 표면의 기하 정보가 없다
    곡면에 따라 텍스처가 변형되는 표현 자체를 할 수 없음
  • 조명·명암 표현이 어렵다
    셀프 합성으로는 빛과 그림자가 가진 깊이를 표현할 수 없음
  • 주름, 굴곡, 재질감 같은 입체적인 요소는 완전히 불가능
    결국 평면 위에 이미지를 붙이는 방식이 전부

이 상태로는 제가 원하는 목업 퀄리티에 절대 도달할 수 없었습니다.

WebGL, Shader

포토샵 없이 웹만으로 포토샵급 결과물을 만들기 위해서는 이미지 합성이 아니라 ‘표면 위에 텍스처를 입히는 과정’에 WebGL, Shader가 필요하다고 판단했습니다.

필요한 정보는 두가지 였습니다.

  • 3D 평면 모델
    텍스처가 얹힐 실제 ‘표면’ 역할을 합니다.
    단순한 2D 사각형이 아니라, UV 좌표를 가진 3D 메쉬가 필요했습니다.
  • UV 좌표
    텍스처의 어떤 부분이 메쉬의 어느 위치에 배치될지 정의합니다.
    텍스처가 곡면을 따라 자연스럽게 휘어지는 핵심 정보입니다.

Canvas 2D는 이런 정보를 다룰 수 없습니다.
반면에, WebGL은 GPU 기반으로 이 모든 계산을 픽셀 단위로 처리할 수 있습니다.

그래서 기술 방향을 조금 틀어서 기존 스택에서 아래와 같은 변화가 있었습니다.

  • Canvas → WebGL로 전환
  • JavaScript → GLSL(Fragment Shader)
  • 2D 합성 → 3D 모델 기반의 UV/노멀 연산 처리
  • PNG 텍스처 → GLTF + Blender 워크플로우 추가

제가 목표로 한 사실적 표현을 위해서는 3D 모델링, 셰이더 코드, GPU 렌더링까지 모두 건드리는 접근이 필수였습니다.


과정

Canvas 2D의 한계를 넘어 WebGL로 전환한 뒤, 실제로 목업 엔진을 완성하기까지 크게 4단계를 거쳤습니다.

  1. WebGL/Shader 파이프라인 구축 - 레이어 분리와 픽셀 단위 연산의 기반
  2. Blender로 3D 모델 제작 - UV 좌표를 직접 만들기 위한 수작업
  3. Multi-pass 렌더링으로 합성 - 분리된 레이어를 하나로 합치는 과정
  4. 실시간 편집 기능 - 드래그, 회전, 스케일이 즉시 반영되는 인터랙션

각 단계마다 예상치 못한 삽질들이 있었고 그 과정에서 배운 것들을 기록합니다.

WebGL/Shader 기반 파이프라인 구축

가장 먼저 해야 할 일은 렌더링 파이프라인의 뼈대를 세우는 것이었습니다.

WebGL을 직접 다루는 건 너무 로우레벨이었습니다. 행렬 연산, 버퍼 관리, 셰이더 컴파일까지 직접 하려면 목업 만드는 게 아니라 그래픽스 엔진을 만드는 셈이죠. Three.js는 이런 복잡함을
추상화하면서도 필요할 때 셰이더를 직접 건드릴 수 있는 유연함이 있습니다.

Three.js의 렌더링은 세 가지 요소로 구성됩니다.

const scene = new THREE.Scene();       // 물체들이 배치되는 공간
const camera = new THREE.PerspectiveCamera(...);  // 어디서 바라볼지
const renderer = new THREE.WebGLRenderer({ canvas, alpha: true });  // 화면에 그리기

이 구조만으로도 간단한 3D 장면은 렌더링할 수 있습니다. 하지만 저는 목업처럼 여러 레이어를 합성해야 하는 상황이라 한 가지 개념이 더 필요했습니다.

일반적인 렌더링은 결과를 바로 화면에 출력합니다. 하지만 저는 여러 재료를 전처리 과정을 하고 각각 따로 그린 뒤 나중에 합쳐야 했습니다.

재료역할
Base 이미지목업의 배경이 되는 원본 사진
Mask 이미지특정 영역을 분리하기 위한 마스크
3D Plane 모델텍스처가 입혀질 메쉬

이걸 가능하게 해주는 게 RenderTarget입니다. 화면 대신 오프스크린 버퍼에 렌더링하고, 나중에 이 버퍼들을 텍스쳐처럼 합성할 수 있습니다.

// 각 레이어를 담을 버퍼 생성
const baseRenderTarget = new THREE.WebGLRenderTarget(width, height);
const maskRenderTarget = new THREE.WebGLRenderTarget(width, height);
const gltfRenderTarget = new THREE.WebGLRenderTarget(width, height);

function animate() {
	// 1. 각 Scene을 별도 버퍼에 렌더링
	renderer.setRenderTarget(baseRenderTarget);
	renderer.render(baseScene, camera);
	
	renderer.setRenderTarget(maskRenderTarget);
	renderer.render(maskScene, camera);
	
	renderer.setRenderTarget(gltfRenderTarget);
	renderer.render(gltfScene, camera);
	
	// 2. 최종 합성 후 화면에 출력
	renderer.setRenderTarget(null);  // null = 실제 화면
	renderer.render(finalScene, camera);
}

앞서 RenderTarget으로 레이어를 분리하는 구조를 만들었습니다. 하지만 각 레이어에서 마스크의 특정 영역만 보이게 같은 세밀한 처리를 하려면, Shader를 직접 다뤄야 했습니다.

셰이더란?

GPU에서 실행되는 작은 프로그램입니다. 화면의 모든 픽셀에 대해 동시에 실행되기 때문에, CPU로 207만 픽셀(1920×1080)을 하나씩 처리하는 것보다 압도적으로 빠릅니다.

Three.js에서 커스텀 셰이더를 사용하려면 ShaderMaterial을 만들어야 하는데, 세 가지 요소로 구성됩니다

const material = new THREE.ShaderMaterial({
	uniforms: { ... },      // 1. 외부에서 전달하는 값
	vertexShader: `...`,    // 2. 정점(꼭짓점) 처리
	fragmentShader: `...`,  // 3. 픽셀 색상 결정
});

1. Uniforms - JavaScript와 셰이더의 다리
Uniforms는 JavaScript에서 셰이더로 값을 전달하는 통로입니다. 텍스처, 숫자, 좌표 등 셰이더가 알아야 할 정보를 넘깁니다.

uniforms: {
	baseTexture: { value: backgroundTexture },  // 배경 이미지
	maskTexture: { value: maskTexture },        // 마스크 이미지
	offset: { value: new Vector2(0, 0) },       // 드래그 오프셋
	rotation: { value: 0 },                     // 회전 각도
	opacity: { value: 1.0 },                    // 투명도
}

나중에 사용자가 텍스처를 드래그하면 material.uniforms.offset.value를 업데이트하고, 셰이더가 이를 반영해서 다시 그립니다.

2. Vertex Shader - UV 좌표 전달
정점(Vertex)의 위치를 처리하는 단계입니다. 목업에서는 복잡한 변환이 필요 없어서, UV 좌표를 Fragment Shader로 넘기는 역할만 합니다.

varying vec2 vUv;  // Fragment Shader로 전달할 변수

void main() {
	vUv = uv;  // 메쉬의 UV 좌표를 그대로 전달
	gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

3. Fragment Shader - 픽셀의 최종 색상 결정
여기가 핵심입니다. 화면의 모든 픽셀에 대해 "이 픽셀은 무슨 색이어야 하는가?"를 계산합니다.

가장 단순한 형태

uniform sampler2D baseTexture;
uniform sampler2D maskTexture;
varying vec2 vUv;

void main() {
	vec4 baseColor = texture2D(baseTexture, vUv);   // 배경 색상 샘플링
	vec4 maskColor = texture2D(maskTexture, vUv);   // 마스크 색상 샘플링
	
	// 마스크의 R채널이 칠해진 영역을 "제외"
	gl_FragColor = vec4(baseColor.rgb, 1.0 - maskColor.r);
}

// - 마스크에서 R채널 영역(r=1.0) → alpha = 0.0 → 투명
// - 마스크에서 R채널을 제외한 영역(r=0.0) → alpha = 1.0 → 보임

삽질 과정

문제원인해결
텍스처가 뒤집혀 보임UV 좌표계의 Y축이 반대vUv.y - (-offset.y)로 보정
회전하면 텍스처가 날아감회전 중심이 (0,0)이었음중심을 (0.5, 0.5)로 이동 후 회전
드래그 방향이 반대좌표계 차이반전 여부에 따라 delta 부호 조정

특히 회전 구현에서 많이 헤맸습니다. 단순히 회전 행렬을 적용하면 UV 원점(0,0) 기준으로 돌아가서 텍스처가 화면 밖으로 날아갑니다.

// 잘못된 방법
vec2 rotatedUv = rotationMatrix * vUv;  // 원점 기준 회전 → 날아감

// 올바른 방법
vec2 centeredUv = vUv - 0.5;             // 1. 중심을 원점으로 이동
vec2 rotatedUv = rotationMatrix * centeredUv;  // 2. 회전
vec2 uv = rotatedUv + 0.5;               // 3. 다시 원위치

3D 모델 만들기 - Blender + GLTF

Fragment Shader로 마스크 처리까지는 해결했습니다. 하지만 한 가지 문제가 남아있었습니다.

평면이 아닌 곡면에는 어떻게 텍스처를 입히지?

티셔츠의 주름, 쿠션의 볼록한 표면, 머그컵의 곡면... 이런 곳에 텍스처를 자연스럽게 입히려면 단순한 2D 좌표로는 불가능합니다. 텍스처가 표면을 따라가야 하는데, 그러려면 표면의 각 지점이
텍스처의 어느 부분과 대응되는지 알아야 합니다.

이게 바로 UV 좌표입니다.

UV 좌표란?

3D 모델의 표면을 2D로 펼친 좌표계입니다. 마치 지구본을 세계지도로 펼치는 것처럼, 3D 메쉬를 2D 평면에 펼쳐서 텍스처를 입힐 수 있게 합니다.

출처 - https://www.meshy.ai/blog/uv-mapping출처 - https://www.meshy.ai/blog/uv-mapping

Blender를 배워야 했던 이유

저는 Plane Geometry를 사용해서 옷의 주름이나 쿠션의 굴곡처럼 표면을 따라 휘어지는 형태를 표현하고 싶었습니다. 그래서 블렌더에서 평면을 만들고 버텍스 등을 조절해 곡면으로 변형하는 과정의 작업을 했습니다.

그래서 Blender를 열었습니다. 프론트엔드 개발자가 3D 모델링 툴을 사용하게 될 줄은 몰랐지만.. UV를 건드려서 제 스스로 만족스러운 결과를 얻기 위해선 선택지가 없었습니다.

Blender에서 하는 작업

  1. Plane 메쉬 생성 - 텍스처가 입혀질 표면
  2. 배경 이미지 배치 - 목업 원본 이미지를 백그라운드로 깔아두기
  3. Subdivision으로 메쉬 분할 - 곡면 표현을 위해 평면을 잘게 쪼개기
  4. 트레이싱 작업 - 트레이싱 페이퍼에 그림 따라 그리듯이.. 배경 이미지 위에서 메쉬를 목업 형태에 맞게 수작업으로 조정
    tracing-paper-image.pngtracing-paper-image.png
  5. 카메라 배치 - Three.js에서 같은 시점으로 렌더링하기 위해
  6. GLTF Export - Three.js가 읽을 수 있는 포맷으로 내보내기
  7. Three.js Editor에서 확인 - export한 모델이 제대로 나왔는지 검증

Modifying 3D model in BlenderModifying 3D model in Blender

결국 디지털 트레이싱 작업이었습니다. 현재 이 지저분한 코드로 자동화할 수 없고 눈과 손으로 하는 저의 집중도에 따라 편차가 있는 목업을 수작업으로 구성했습니다..

블렌더라는 툴의 존재 자체를 이때 알았기 때문에 블렌더 지식은 전무했습니다.
목업의 퀄리티가 낮은 이유는 애석하게도 당연한 결과입니다🙄

Three.js에서 GLTF 로딩
위 과정을 통해 모델을 만들어 냈다면 코드단에서는 이렇게 GLTFLoader를 사용해서 모델을 읽어냅니다.

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';

const loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);  // 압축된 GLTF 지원

loader.load(modelPath, (gltf) => {
gltf.scene.traverse((child) => {
  // 카메라 추출 - Blender에서 잡은 시점 그대로 사용
  if (child.isCamera) {
	camera = child;
  }

  // 메쉬 추출 - 여기에 커스텀 Shader Material 적용
  if (child.isMesh) {
	child.material = customShaderMaterial;
  }
});

scene.add(gltf.scene);
});

삽질 포인트

  • 모델이 화면에 안 보임

    • 원인 - 스케일이 너무 작거나 큼
    • 해결 - Blender에서 스케일 맞추거나 Three.js 코드단에서 조정
  • 텍스처가 이상하게 찢어짐

    • 원인 - UV 시임(Seam) 위치 문제
    • 해결 - Blender에서 UV 시임 재조정
  • 카메라 시점이 다름

    • 원인 - Blender/Three.js 좌표계 차이
    • 해결 - 카메라를 GLTF에 포함시켜서 export. 이때 GLB와 GLTF의 차이를 경험

특히 목업 하나를 추가할 때마다 이 과정을 거쳐야 했습니다.

새 목업 추가 시 워크플로우

  1. Blender에서 모델링
  2. UV 펼치기 (가장 오래 걸림)
  3. 카메라 위치 잡기
  4. GLTF export
  5. Three.js에서 로딩 테스트
  6. Shader 연동 확인
  7. 미세 조정 반복...

자동화할 수 없는 한땀한땀의 과정이었습니다. 그래서 목업 하나하나에 애착이 갔습니다.(고난이도 목업은 나의 부족함을 인정하고 합리화 가능한 부분)
한두개씩 목업을 완성해갈때마다 제 손이 익어서 작업 시간이 단축될 뿐이었습니다.😇
하지만 누군가에겐 단 한번의 드래그로 퀄리티 있는 목업을 사용할 수 있다면 사용자 위한 마인드로 임했습니다.


최종 합성 - Multi-pass 렌더링

각 레이어를 별도 RenderTarget에 렌더링했으니 이제 하나로 합쳐야 합니다.

baseRenderTarget → Base + 마스크 영역
maskRenderTarget → Base + 마스크 제외 영역
gltfRenderTarget → 3D Plane + 사용자 텍스처
↓
finalScene에서 합성

Final Shader 합성 로직

void main() {
	vec4 baseColor = texture2D(baseTexture, vUv);
	vec4 maskColor = texture2D(maskTexture, vUv);
	vec4 gltfColor = texture2D(gltfTexture, vUv);
	
	// 마스크 영역에 GLTF 텍스처를 곱해서 합성
	vec3 enhancedMaskColor = maskColor.rgb * gltfColor.rgb * maskColor.a;
	
	gl_FragColor = vec4(baseColor.rgb + enhancedMaskColor, 1.0);
}

여기서 쉐이더 핵심은 maskColor.rgb * gltfColor.rgb입니다. 단순히 덮어씌우는 게 아니라 곱하기 블렌딩을 해서 원본 이미지의 명암이 텍스처에 자연스럽게 반영됩니다.


실시간 텍스처 편집

그래서 지금까지 만든걸 사용자가 어떻게 쓸 수 있는데?

사용자가 텍스처를 조작할 수 있어야 진짜 편집 도구입니다.

구현한 기능들

기능방식
드래그마우스 이동량 → UV offset
회전각도 → 2D 회전 행렬
스케일0.1x ~ 3.0x 범위 조절
반전좌우/상하 flip

모든 조작이 Uniform 값만 업데이트하면 Shader가 알아서 다시 그려줍니다.

// 드래그 시
material.uniforms.offset.value.set(newX, newY);

// 회전 시
material.uniforms.rotation.value = angle;

// 스케일 시
material.uniforms.textureScale.value = scale;

CPU에서 픽셀을 하나하나 다시 계산하는 게 아니라 GPU가 병렬로 처리하니까 실시간 프리뷰가 가능합니다. 드래그하면 즉시 반영되는 반응성이 여기서 나옵니다!

Mocus poster editor demoMocus poster editor demo


(번외) UI/UX 인터랙션 기능 구현

WebGL 렌더링만으로는 도구가 아닙니다. 사용자가 직관적으로 조작할 수 있는 인터랙션이 있어야 진짜 제품이 됩니다.

드래그 앤 드롭(DnD) - 파일 업로드

const onDrop = (e: DragEvent) => {
	e.preventDefault();
	const file = e.dataTransfer?.files[0];
	if (file?.type.startsWith('image/')) {
	  const url = URL.createObjectURL(file);
	  updateTexture(url);
	}
};

브라우저에 이미지를 끌어다 놓으면 바로 목업에 반영됩니다. 파일 선택 버튼 누르는 것보다 훨씬 직관적입니다.

호버 감지 - Raycast

마우스가 텍스처 영역 위에 있을 때만 드래그가 가능하도록 해야 합니다.

const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();

const onMouseMove = (e: MouseEvent) => {
	mouse.x = (e.clientX / width) * 2 - 1;
	mouse.y = -(e.clientY / height) * 2 + 1;
	
	raycaster.setFromCamera(mouse, camera);
	const intersects = raycaster.intersectObject(overlayMesh);
	
	setIsHover(intersects.length > 0);
};

3D 공간에서 마우스 위치를 확인해서 메쉬와 교차하는지 판단합니다. 호버 시 테두리 표시도 이걸로 구현했습니다.

키보드 단축키

키기능
← →1도씩 회전
Shift + ← →90도씩 회전
Space + 드래그캔버스 전체 이동
const onKeyDown = (e: KeyboardEvent) => {
	if (e.key === 'ArrowLeft') {
	  setRotation(prev => prev - (e.shiftKey ? 90 : 1));
	}
};

마우스로 미세 조정이 어려울 때 키보드가 빛을 발합니다.

상태 관리 - Context API

저는 상태관리에 대해서는 생각보다 올드한 마인드를 가지고 있습니다.
Redux, Recoil, Zuztand, 여타 상태관리 라이브러리가 많지만 저는 항상 상황에 맞게 필요에 따라 적재적소에 사용하려 합니다.(저는 오버 엔지니어링이 싫어요)

당장은 전역적으로 사용한다거나 할 상황도 아니고 고급 상태 관리를 하지 않는다 판단했기에 가장 기본이 된다고 생각하는 Context API를 선택했습니다.

Canvas, Sidebar, 각종 Hook들이 같은 상태를 공유해야 합니다.
그래서 총 4개의 Provider로 역할을 분리했습니다.

App
└─ CanvasConfigProvider      // 목업별 설정
   └─ CanvasSettingsProvider   // Three.js 핵심 객체
	   └─ CanvasPropertiesProvider  // 캔버스 뷰 상태
		   └─ TextureProvider          // 텍스처 관리
			   └─ 실제 컴포넌트들
Provider역할주요 상태
CanvasConfig목업별 고정 설정baseImg, maskImg, material, 반전/채우기 옵션
CanvasSettingsThree.js 객체 참조renderer, scene, camera, overlayMesh refs
CanvasProperties캔버스 뷰 조작캔버스 위치(x,y), 줌 스케일
Texture현재 사용자가 주입한 이미지 텍스처현재 텍스처, 텍스처 리스트, 전환 함수

왜 이렇게 나눴는가?

// CanvasSettings - Three.js 객체는 ref로 관리
const rendererRef = useRef<WebGLRenderer>(null);
const overlayRef = useRef<Mesh>(null);

// Texture - 사용자 조작에 따라 자주 바뀌는 상태
const [textureList, setTextureList] = useState<Texture[]>([]);

// - ref로 관리하는 것: Three.js 객체들. 값이 바뀌어도 리렌더링이 필요 없음
// - state로 관리하는 것: UI에 반영되어야 하는 것들. 값이 바뀌면 리렌더링

// Texture Provider 활용 예시

// 텍스처 업로드 시
const { updateTexture } = useTexture();
updateTexture({ name: 'logo.png', image: imageUrl });

// 사이드바에서 텍스처 전환 시
const { textureList, switchTexture } = useTexture();
switchTexture(selectedTexture);  // 선택한 텍스처를 맨 앞으로

이렇게 상태관리를 간단한 형태로 구성하고 Props drilling 지옥을 피하면서, 각 관심사별로 상태를 분리해서 관리할 수 있습니다.


결과 - 그래서 무엇을 만들어 냈는가

렌더링 파이프라인

  • Multi-pass 렌더링으로 Base, Mask, GLTF 레이어 분리
  • Fragment Shader에서 채널별 마스킹과 Multiply 블렌딩
  • GPU 병렬 처리로 실시간 프리뷰 가능

3D 모델 워크플로우

  • Blender에서 트레이싱 방식으로 Plane 메쉬 제작
  • UV 좌표를 직접 잡아서 곡면 표현

사용자 인터랙션

  • 드래그로 텍스처 위치 조정
  • 회전, 스케일, 반전 실시간 적용
  • 드래그 앤 드롭으로 이미지 업로드
  • 고해상도 PNG 다운로드

고급 기능

  • AI 이미지 배경 제거

[이미지 나열해야지]

티셔츠, 쿠션, 토트백 같은 의류부터 명함, 패키징, 디바이스, 빌보드, 사이니지까지 하나하나 Blender에서 UV 작업을 거쳐 만들었습니다.


마무리

이 도구에는 분명히 한계가 있습니다.
목업 하나를 추가하려면 Blender 모델링 → UV 작업 → GLTF export → 셰이더 연동 등의 프로세스가 많았기 때문에 자동화할 수 없는 한땀한땀의 과정을 거쳐야 합니다. 그래서 이 전과정을 거친 목업은 제 손이 닿은 만큼 애착이 갔습니다.
그래도 처음 세웠던 가설인 '브라우저에서 포토샵급 목업이 가능할까?'로 시작된 프로젝트는 실제로 가능함을 검증했다고 생각합니다.

그리고 대AI의 시대가 왔습니다..
지금은 AI 프롬프팅으로 순식간에 세상에 없는 제품을 목업화하고, 드래그 한두번으로 광고 영상제작 까지도 순식간에 해냅니다.
허깅페이스에 쏟아지는 수많은 고성능 오픈소스 모델들과 여러 플랫폼에서 '딸깍' 한번으로 제가 상상하는 목업을 만들어 낼수 있음에 충격과 허탈감이 동시에 찾아오기도 했습니다.
제가 만든 도구는 순식간에 AI에 대체될 미래가 보였기 때문에 프로젝트를 중단하기로 마음 먹게 되었습니다.

하지만 이러한 기술의 변화 속도에 놀라면서도 기본기(셰이더, 3D)의 가치는 여전하다고 느낍니다.
AI도 결국은 마스킹, 텍스처 블렌딩 같은 기술들을 학습한 결과물입니다. 화려한 결과 뒤에는 제가 이 프로젝트에서 삽질하며 배운 것들과 같은 원리가 깔려 있다고 생각합니다.

AI가 무엇을 해주는지는 금방 배울 수 있지만 어떻게 작동하는지를 아는 건 다른 문제입니다.
저는 엔지니어로서 이 프로젝트를 통해 그 근본적인 영역을 직접 경험했다는 것에 만족합니다.

아래 링크는 제가 위 기술을 적용한 프로젝트 Mocus입니다! 따끔한 피드백도 언제든 환영입니다.
Mocus - 드래그 한번으로 목업을 제작해보세요!

궁금하신 점이나 도움이 필요하시면 언제든 연락 부탁드립니다.
dev.bearjb@gmail.com


  • 개요
  • 왜 이 프로젝트를 시작 했었는가
    • 포토샵 의존을 벗어난 웹 기반 목업 엔진이 필요했다
    • “정말 사실적인 목업”이라는 기준
    • 개발자로서 ‘만들 수 있는가’를 스스로 확인하고 싶었다
  • Canvas API vs WebGL Shader - 선택의 갈림길
    • Canvas 2D로 만든 첫 프로토타입
    • WebGL, Shader
  • 과정
    • WebGL/Shader 기반 파이프라인 구축
      • 셰이더란?
    • 3D 모델 만들기 - Blender + GLTF
      • UV 좌표란?
      • Blender를 배워야 했던 이유
    • 최종 합성 - Multi-pass 렌더링
    • 실시간 텍스처 편집
    • (번외) UI/UX 인터랙션 기능 구현
      • 드래그 앤 드롭(DnD) - 파일 업로드
      • 호버 감지 - Raycast
      • 상태 관리 - Context API
  • 결과 - 그래서 무엇을 만들어 냈는가
  • 마무리