🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Three.js 고급 코드 분석 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 29. · 3 Views

Three.js 고급 코드 분석 완벽 가이드

Three.js 공식 예제를 분석하고 복잡한 씬 구조를 이해하는 방법부터 커스텀 Shader, PostProcessing, 성능 최적화, 디버깅까지 실무에서 바로 활용할 수 있는 고급 기법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 상황 스토리와 비유로 풀어낸 가이드입니다.


목차

  1. Three.js 공식 예제 분석 방법
  2. 복잡한 씬 구조 이해하기
  3. 커스텀 Shader 기초
  4. PostProcessing 효과
  5. 성능 프로파일링 및 최적화
  6. 디버깅 도구 활용

1. Three.js 공식 예제 분석 방법

입사 6개월 차 김개발 씨는 처음으로 3D 웹 프로젝트를 맡게 되었습니다. Three.js 공식 사이트에 들어가 멋진 예제들을 보며 감탄했지만, 막상 코드를 열어보니 머리가 하얘졌습니다.

"이 복잡한 코드를 어떻게 이해하지?"

Three.js 공식 예제 분석은 실무 수준의 3D 웹 개발 능력을 키우는 가장 효과적인 학습 방법입니다. 공식 예제는 Best Practice를 담고 있으며, 체계적인 분석 방법을 익히면 복잡한 프로젝트도 쉽게 이해할 수 있습니다.

마치 요리 레시피를 보듯이, 코드의 재료와 순서를 파악하는 것이 핵심입니다.

다음 코드를 살펴봅시다.

// Three.js 예제 분석 체크리스트
const analysisChecklist = {
  // 1단계: 초기화 및 설정 파악
  setup: ['Scene', 'Camera', 'Renderer', 'Controls'],

  // 2단계: 3D 객체 및 재질 분석
  objects: ['Geometry', 'Material', 'Mesh', 'Light'],

  // 3단계: 애니메이션 로직 추적
  animation: ['requestAnimationFrame', 'update methods', 'time delta'],

  // 4단계: 이벤트 및 인터랙션 확인
  interaction: ['resize', 'mouse events', 'keyboard input']
};

// 예제 분석 시작점 찾기
function analyzeExample(exampleCode) {
  // init() 함수부터 시작하는 경우가 많음
  const initFunction = findFunction(exampleCode, 'init');
  const animateFunction = findFunction(exampleCode, 'animate');

  return { initFunction, animateFunction };
}

김개발 씨는 선배 개발자인 박시니어 씨에게 도움을 청했습니다. "선배님, Three.js 예제 코드가 너무 복잡해서 어디서부터 봐야 할지 모르겠어요." 박시니어 씨는 웃으며 대답했습니다.

"처음엔 다 그래요. 하지만 규칙만 알면 의외로 간단합니다." 체계적인 분석의 중요성 Three.js 예제를 분석하는 것은 마치 요리 레시피를 읽는 것과 같습니다.

재료 준비, 손질, 조리, 플레이팅 순서가 있듯이, Three.js 코드도 정해진 패턴이 있습니다. 이 패턴만 익히면 어떤 복잡한 예제도 쉽게 이해할 수 있습니다.

대부분의 Three.js 예제는 비슷한 구조를 따릅니다. 먼저 Scene, Camera, Renderer를 초기화하고, 3D 객체를 추가한 다음, 애니메이션 루프를 돌립니다.

이 흐름을 이해하는 것이 첫 번째 단계입니다. 1단계: 초기화 코드 찾기 박시니어 씨가 코드를 가리키며 설명했습니다.

"먼저 init 함수나 파일 상단의 초기화 코드를 찾아보세요. 여기서 무대 설정이 이루어집니다." 초기화 단계에서는 Scene(무대), Camera(카메라), Renderer(렌더러)가 생성됩니다.

이 세 가지는 Three.js의 필수 요소입니다. 마치 영화 촬영에서 세트장, 카메라, 조명이 필요한 것과 같습니다.

또한 OrbitControlsTransformControls 같은 컨트롤러가 추가되는지 확인해야 합니다. 이것들은 사용자가 3D 씬을 조작할 수 있게 해주는 도구입니다.

2단계: 3D 객체 생성 부분 분석 "다음은 어떤 3D 객체가 만들어지는지 보는 겁니다." 박시니어 씨가 계속 설명했습니다. 예제 코드에서 Geometry(형태)와 Material(재질)을 찾아보세요.

BoxGeometry면 상자, SphereGeometry면 구입니다. Material은 MeshStandardMaterial, MeshPhongMaterial 등 다양합니다.

이 둘을 합쳐서 Mesh를 만들고 Scene에 추가합니다. 조명도 중요합니다.

DirectionalLight(태양광), PointLight(전구), AmbientLight(환경광) 등이 어디에 어떻게 배치되어 있는지 확인하세요. 조명 없이는 아무것도 보이지 않습니다.

3단계: 애니메이션 로직 추적 "이제 가장 중요한 부분입니다. animate 함수를 찾아보세요." Three.js는 requestAnimationFrame을 사용해서 매 프레임마다 화면을 다시 그립니다.

animate 함수 안에서 객체의 위치, 회전, 크기가 변경되고, renderer.render()가 호출되어 화면에 표시됩니다. 시간 기반 애니메이션을 사용하는 경우 Clock 객체나 delta time을 확인하세요.

이것은 프레임레이트와 무관하게 일정한 속도로 애니메이션이 재생되도록 해줍니다. 4단계: 이벤트 및 인터랙션 확인 마지막으로 사용자 입력이나 화면 크기 변경 같은 이벤트 처리를 확인합니다.

window.addEventListener('resize', onWindowResize) 같은 코드를 찾아보세요. 마우스 클릭, 키보드 입력, 터치 제스처 등 어떤 인터랙션이 구현되어 있는지 파악하면 예제의 전체 그림이 완성됩니다.

실전 분석 사례 김개발 씨가 직접 예제를 분석해봤습니다. webgl_animation_keyframes 예제를 열어서 단계별로 따라갔습니다.

init 함수에서 Scene, PerspectiveCamera, WebGLRenderer를 만들고, GLTFLoader로 3D 모델을 불러오는 것을 확인했습니다. animate 함수에서는 AnimationMixer로 애니메이션을 재생하고 있었습니다.

"아, 이렇게 보니까 이해가 되네요!" 주의할 점 처음부터 모든 코드를 이해하려고 하지 마세요. 먼저 큰 흐름을 파악하고, 관심 있는 부분부터 깊이 들어가는 것이 효율적입니다.

또한 예제마다 사용하는 라이브러리나 로더가 다를 수 있습니다. import 문을 잘 확인해서 어떤 모듈을 사용하는지 파악하세요.

정리 박시니어 씨가 마무리했습니다. "이제 알겠죠?

Three.js 예제는 정해진 패턴이 있어요. 초기화 → 객체 생성 → 애니메이션 → 인터랙션 순서로 분석하면 됩니다." 김개발 씨는 자신감이 생겼습니다.

앞으로 어떤 예제를 보더라도 체계적으로 분석할 수 있을 것 같습니다.

실전 팁

💡 - 브라우저 개발자 도구의 Sources 탭에서 브레이크포인트를 걸어 코드 실행 흐름을 단계별로 확인하세요

  • Three.js 공식 문서를 옆에 켜두고, 모르는 클래스나 메서드를 바로바로 검색하세요
  • 예제를 복사해서 로컬에서 실행하며 값을 바꿔보면 훨씬 빠르게 이해할 수 있습니다

2. 복잡한 씬 구조 이해하기

김개발 씨가 처음으로 맡은 프로젝트는 3D 제품 쇼룸 웹사이트였습니다. 수십 개의 제품 모델, 조명, 환경, UI 요소들이 한 화면에 표시되어야 했습니다.

"이렇게 많은 객체를 어떻게 관리하지?"

**씬 구조(Scene Graph)**는 3D 객체들을 계층적으로 관리하는 트리 구조입니다. 마치 회사 조직도처럼 부모-자식 관계로 객체를 구성하면, 복잡한 씬도 체계적으로 관리할 수 있습니다.

Group 객체를 활용하면 관련된 객체들을 묶어서 한 번에 제어할 수 있으며, 이는 대규모 프로젝트의 핵심 패턴입니다.

다음 코드를 살펴봅시다.

// 씬 구조 체계적으로 구성하기
import * as THREE from 'three';

const scene = new THREE.Scene();

// 1. 환경 그룹 (조명, 배경)
const environmentGroup = new THREE.Group();
environmentGroup.name = 'Environment';
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
environmentGroup.add(ambientLight, directionalLight);

// 2. 제품 그룹 (여러 제품 모델)
const productsGroup = new THREE.Group();
productsGroup.name = 'Products';
// 각 제품은 또 하위 그룹을 가질 수 있음
const product1 = new THREE.Group();
product1.add(new THREE.Mesh(geometry, material));

// 3. UI 요소 그룹 (라벨, 아이콘)
const uiGroup = new THREE.Group();
uiGroup.name = 'UI';

// 씬에 최상위 그룹만 추가
scene.add(environmentGroup, productsGroup, uiGroup);

// 특정 그룹 찾기
const products = scene.getObjectByName('Products');
products.visible = false; // 그룹 전체 숨기기

박시니어 씨가 김개발 씨의 화면을 보며 고개를 저었습니다. "이렇게 모든 객체를 scene에 바로 추가하면 나중에 관리가 불가능해요." 김개발 씨는 당황했습니다.

"그럼 어떻게 해야 하나요?" 씬 그래프의 개념 Three.js의 씬 그래프는 마치 컴퓨터의 폴더 구조와 같습니다. 루트 폴더(Scene) 아래에 여러 하위 폴더(Group)를 만들고, 그 안에 파일(Mesh)을 넣는 방식입니다.

예를 들어 자동차 3D 모델을 생각해봅시다. 자동차는 차체, 바퀴 4개, 문 4개로 구성됩니다.

이것을 평평하게 나열하면 관리가 어렵습니다. 하지만 계층 구조로 만들면 "바퀴 그룹을 회전시켜라"처럼 한 번에 제어할 수 있습니다.

Group 객체의 위력 박시니어 씨가 코드를 수정하며 설명했습니다. "Group 객체를 사용하면 논리적으로 묶을 수 있어요." Group은 아무것도 렌더링하지 않는 빈 컨테이너입니다.

하지만 이 컨테이너에 담긴 모든 객체는 Group의 위치, 회전, 크기 변환을 상속받습니다. 마치 상자에 물건을 담으면, 상자를 옮길 때 물건도 함께 움직이는 것과 같습니다.

실무에서는 보통 역할별로 그룹을 나눕니다. Environment(환경), Models(3D 모델), UI(사용자 인터페이스), Effects(특수 효과) 같은 식입니다.

계층 구조의 장점 "왜 굳이 이렇게 복잡하게 하나요?" 김개발 씨가 물었습니다. 박시니어 씨는 실제 사례를 들었습니다.

"쇼룸에서 모든 제품을 한 번에 숨겨야 한다고 생각해보세요. 그룹 없이는 수십 개 객체를 일일이 찾아서 visible을 false로 바꿔야 합니다.

하지만 그룹이 있다면?" productsGroup.visible = false 한 줄이면 끝입니다. 또한 productsGroup.position.y += 1처럼 전체를 한 번에 이동시킬 수도 있습니다.

부모-자식 관계의 변환 계층 구조에서 가장 중요한 개념은 상대적 변환입니다. 자식 객체의 position, rotation, scale은 부모를 기준으로 합니다.

예를 들어 자동차 그룹을 (10, 0, 0)으로 이동시키면, 그 안의 모든 바퀴와 문도 함께 10만큼 이동합니다. 하지만 바퀴의 position은 여전히 (0, 0, 0)입니다.

자동차를 기준으로 (0, 0, 0)이라는 뜻입니다. 이것은 매우 강력한 기능입니다.

복잡한 수학 계산 없이도 객체들을 논리적으로 묶어서 제어할 수 있습니다. 씬 탐색과 검색 "특정 객체를 찾으려면 어떻게 하나요?" 김개발 씨가 물었습니다.

Three.js는 getObjectByName, getObjectById, traverse 같은 메서드를 제공합니다. name 속성을 지정해두면 나중에 쉽게 찾을 수 있습니다.

traverse는 씬 그래프 전체를 순회하며 조건에 맞는 객체를 찾을 때 유용합니다. scene.traverse(obj => { if (obj.isMesh) { ...

} }) 형태로 사용합니다. 실전 활용 사례 김개발 씨는 프로젝트를 다시 구조화했습니다.

제품마다 하나의 Group을 만들고, 그 안에 메시, 라벨, 하이라이트 효과를 넣었습니다. 제품을 클릭하면 해당 그룹만 회전시키면 됩니다.

또한 씬을 "메인 뷰", "디테일 뷰", "비교 뷰" 같은 레이어로 나눠서 관리했습니다. 각 뷰는 최상위 그룹으로 만들고, 뷰 전환 시 해당 그룹만 보이도록 했습니다.

주의할 점 너무 깊은 계층 구조는 오히려 복잡성을 증가시킵니다. 보통 3~4단계 정도가 적당합니다.

또한 부모 객체에 scale을 적용할 때 주의해야 합니다. 자식 객체도 함께 늘어나기 때문에, 의도하지 않은 결과가 나올 수 있습니다.

정리 박시니어 씨가 마무리했습니다. "씬 구조는 코드의 설계도입니다.

처음에 잘 설계해두면 나중에 기능 추가나 수정이 훨씬 쉬워집니다." 김개발 씨는 이제 복잡한 씬도 자신 있게 관리할 수 있게 되었습니다.

실전 팁

💡 - 그룹에는 반드시 의미 있는 name을 지정해서 나중에 쉽게 찾을 수 있게 하세요

  • scene.traverse()로 전체 씬 구조를 출력해보면 계층 관계를 한눈에 파악할 수 있습니다
  • Layers를 활용하면 카메라가 특정 그룹만 렌더링하도록 제어할 수 있어 성능 최적화에 유용합니다

3. 커스텀 Shader 기초

김개발 씨는 디자이너로부터 특이한 요청을 받았습니다. "물결치는 효과에 홀로그램 느낌을 더하고, 마우스를 따라 빛이 움직이게 해주세요." Three.js의 기본 Material로는 불가능한 요구사항이었습니다.

"이건 어떻게 만들지?"

커스텀 Shader는 GPU에서 직접 실행되는 프로그램으로, 독창적인 시각 효과를 만들 수 있습니다. Vertex Shader는 정점의 위치를 변형하고, Fragment Shader는 픽셀의 색상을 결정합니다.

ShaderMaterial을 사용하면 완전히 새로운 재질을 만들 수 있으며, 이것이 고급 3D 표현의 핵심입니다.

다음 코드를 살펴봅시다.

// 기본 커스텀 Shader 구조
import * as THREE from 'three';

const vertexShader = `
  // Vertex Shader: 정점 위치 변환
  varying vec2 vUv; // Fragment Shader로 전달할 UV 좌표

  void main() {
    vUv = uv; // UV 좌표 저장

    // 정점 위치 계산 (모델 → 월드 → 뷰 → 클립 공간)
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

const fragmentShader = `
  // Fragment Shader: 픽셀 색상 결정
  uniform float uTime; // JavaScript에서 전달받는 시간 값
  varying vec2 vUv; // Vertex Shader에서 받은 UV 좌표

  void main() {
    // UV 좌표와 시간을 이용한 색상 계산
    vec3 color = vec3(vUv.x, vUv.y, sin(uTime) * 0.5 + 0.5);
    gl_FragColor = vec4(color, 1.0);
  }
`;

const shaderMaterial = new THREE.ShaderMaterial({
  vertexShader,
  fragmentShader,
  uniforms: {
    uTime: { value: 0.0 } // 애니메이션용 시간 변수
  }
});

// 애니메이션 루프에서 시간 업데이트
function animate(time) {
  shaderMaterial.uniforms.uTime.value = time * 0.001;
}

박시니어 씨가 웃으며 말했습니다. "드디어 Shader를 배울 때가 왔네요.

조금 어렵지만, 이걸 알면 표현의 한계가 사라집니다." 김개발 씨는 긴장했습니다. "Shader가 뭔가요?

GLSL이라는 것도 들어봤는데..." Shader란 무엇인가 Shader는 마치 포토샵 필터와 같습니다. 하지만 포토샵은 CPU에서 느리게 처리되는 반면, Shader는 GPU에서 병렬로 빠르게 실행됩니다.

Three.js의 기본 Material(MeshStandardMaterial 등)도 사실 내부적으로 Shader를 사용합니다. 하지만 우리가 직접 Shader를 작성하면 완전히 새로운 효과를 만들 수 있습니다.

Shader는 GLSL(OpenGL Shading Language)이라는 C 비슷한 언어로 작성됩니다. JavaScript와는 문법이 다르지만, 기본 개념은 비슷합니다.

Vertex Shader와 Fragment Shader 박시니어 씨가 그림을 그리며 설명했습니다. "렌더링은 두 단계로 이루어집니다." 첫 번째는 Vertex Shader입니다.

3D 모델의 각 정점 위치를 계산합니다. 예를 들어 평면을 휘어지게 만들거나, 물결치는 효과를 주려면 여기서 정점을 움직입니다.

두 번째는 Fragment Shader(또는 Pixel Shader)입니다. 정점 사이의 모든 픽셀 색상을 결정합니다.

그라데이션, 패턴, 텍스처 효과는 여기서 만듭니다. 데이터 전달: Uniforms와 Varyings "JavaScript에서 Shader로 데이터를 어떻게 보내나요?" 김개발 씨가 물었습니다.

Uniforms는 JavaScript에서 Shader로 전달하는 전역 변수입니다. 시간, 마우스 위치, 색상 같은 값을 보냅니다.

모든 정점과 픽셀에 동일하게 적용됩니다. Varyings는 Vertex Shader에서 Fragment Shader로 전달하는 변수입니다.

UV 좌표, 법선 벡터 같은 값을 보냅니다. GPU가 자동으로 보간해서 각 픽셀마다 다른 값을 만듭니다.

첫 번째 커스텀 Shader 만들기 박시니어 씨가 간단한 예제를 보여줬습니다. "UV 좌표를 색상으로 표현하는 Shader입니다." 위 코드에서 Vertex Shader는 UV 좌표를 vUv에 저장하고, Fragment Shader는 그것을 RGB 값으로 사용합니다.

vUv.x는 가로 위치(빨강), vUv.y는 세로 위치(초록)가 됩니다. uTime은 JavaScript에서 계속 업데이트되는 시간 값입니다.

sin(uTime)으로 파란색이 시간에 따라 변합니다. ShaderMaterial 사용법 Three.js에서 커스텀 Shader를 사용하려면 ShaderMaterial을 만듭니다.

vertexShader와 fragmentShader 문자열을 전달하고, uniforms 객체로 변수를 정의합니다. uniforms의 각 속성은 { value: ...

} 형태로 작성합니다. 애니메이션 루프에서 .value를 변경하면 실시간으로 Shader에 반영됩니다.

실전 활용 사례 김개발 씨는 물결 효과를 만들어봤습니다. Vertex Shader에서 position.z += sin(position.x + uTime) 같은 식으로 정점을 위아래로 움직였습니다.

파도가 치는 것처럼 보였습니다. Fragment Shader에서는 vUv 좌표에 따라 색상을 변경해서 홀로그램 느낌을 냈습니다.

디자이너가 감탄했습니다! 디버깅과 학습 Shader 디버깅은 어렵습니다.

console.log를 쓸 수 없기 때문입니다. 대신 색상으로 값을 확인합니다.

gl_FragColor = vec4(someValue, 0.0, 0.0, 1.0) 같은 식으로 빨간색 강도로 값을 시각화합니다. ShaderToy, The Book of Shaders 같은 온라인 리소스에서 다양한 예제를 보며 배울 수 있습니다.

주의할 점 Shader는 GPU에서 실행되므로 매우 빠르지만, 너무 복잡한 계산은 성능을 떨어뜨립니다. 특히 Fragment Shader는 모든 픽셀마다 실행되므로 최적화가 중요합니다.

또한 GLSL 문법은 엄격합니다. 세미콜론을 빼먹거나 타입이 맞지 않으면 에러가 발생하며, 에러 메시지가 불친절한 편입니다.

정리 박시니어 씨가 격려했습니다. "Shader는 처음엔 어렵지만, 익숙해지면 무한한 가능성이 열립니다.

조금씩 실험하면서 배워보세요." 김개발 씨는 신기한 효과를 만들 수 있다는 사실에 흥분했습니다.

실전 팁

💡 - Shader 작성 시 Three.js가 제공하는 내장 변수(position, uv, normal, modelViewMatrix 등)를 활용하세요

  • 복잡한 Shader는 작은 부분부터 만들어 색상으로 확인하며 단계적으로 완성하세요
  • ShaderMaterial 대신 onBeforeCompile 훅을 사용하면 기존 Material에 커스텀 코드를 삽입할 수 있습니다

4. PostProcessing 효과

프로젝트가 거의 완성되었을 때, 클라이언트가 피드백을 주었습니다. "좋긴 한데 뭔가 밋밋해요.

게임처럼 화려한 효과를 넣을 수 있나요?" 김개발 씨는 Bloom, 모션 블러, 색 보정 같은 효과를 추가해야 했습니다.

PostProcessing은 렌더링된 화면 전체에 필터 효과를 적용하는 기법입니다. 마치 사진 앱에서 필터를 씌우듯이, 3D 씬을 먼저 그린 후 그 결과물에 Bloom, SSAO, Color Grading 같은 효과를 추가합니다.

EffectComposer를 사용하면 여러 효과를 파이프라인으로 연결할 수 있으며, 영화 같은 퀄리티를 만드는 핵심 기술입니다.

다음 코드를 살펴봅시다.

// PostProcessing 기본 설정
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';

// 1. EffectComposer 생성
const composer = new EffectComposer(renderer);

// 2. 기본 렌더링 Pass (필수)
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);

// 3. Bloom 효과 추가 (발광 효과)
const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.5,  // strength (강도)
  0.4,  // radius (반경)
  0.85  // threshold (임계값)
);
composer.addPass(bloomPass);

// 4. 안티앨리어싱 Pass (마지막에 추가)
const smaaPass = new SMAAPass(window.innerWidth, window.innerHeight);
composer.addPass(smaaPass);

// 애니메이션 루프에서 renderer.render() 대신 사용
function animate() {
  composer.render(); // EffectComposer로 렌더링
}

박시니어 씨가 화면을 보며 고개를 끄덕였습니다. "3D 모델링은 잘 되어 있는데, 후처리 효과가 없으니 좀 심심하네요." 김개발 씨가 물었습니다.

"후처리 효과요? 그게 뭔가요?" PostProcessing의 개념 PostProcessing은 마치 인스타그램 필터와 같습니다.

사진을 찍은 후에 밝기, 대비, 채도를 조정하는 것처럼, 3D 씬을 렌더링한 후에 화면 전체에 효과를 적용합니다. 게임이나 영화에서 보는 Bloom(발광), DOF(피사체 심도), Motion Blur(모션 블러) 같은 효과가 모두 PostProcessing입니다.

Three.js로 렌더링한 기본 화면에 이런 효과를 더하면 퀄리티가 극적으로 향상됩니다. EffectComposer의 역할 박시니어 씨가 설명했습니다.

"PostProcessing을 사용하려면 먼저 EffectComposer를 이해해야 합니다." 일반적으로는 renderer.render(scene, camera)로 화면에 바로 그립니다. 하지만 PostProcessing을 쓸 때는 EffectComposer가 중간에 개입합니다.

EffectComposer는 여러 개의 Pass(처리 단계)를 순서대로 실행합니다. 첫 번째 Pass가 씬을 렌더링하고, 그 결과를 두 번째 Pass가 받아서 Bloom 효과를 추가하고, 세 번째 Pass가 또 다른 효과를 추가하는 식입니다.

마치 공장의 조립 라인과 같습니다. 기본 구조: RenderPass 모든 PostProcessing 파이프라인은 RenderPass로 시작합니다.

이것은 scene과 camera를 받아서 기본 렌더링을 수행합니다. RenderPass 없이 다른 효과만 추가하면 아무것도 나오지 않습니다.

반드시 첫 번째로 추가해야 합니다. Bloom 효과 추가하기 "가장 인기 있는 효과는 Bloom입니다." 박시니어 씨가 UnrealBloomPass를 추가했습니다.

Bloom은 밝은 부분이 주변으로 번지는 발광 효과입니다. 네온사인, 레이저, 마법 같은 표현에 필수입니다.

UnrealBloomPass는 세 가지 주요 파라미터가 있습니다. strength(강도)는 효과의 세기, radius(반경)는 번지는 범위, threshold(임계값)는 어느 정도 밝기부터 효과를 적용할지 결정합니다.

strength를 너무 높이면 화면이 온통 하얗게 날아가고, threshold를 낮추면 모든 부분이 빛납니다. 적절한 값을 찾는 게 중요합니다.

다양한 PostProcessing Pass Three.js는 다양한 내장 Pass를 제공합니다. SSAOPass(Screen Space Ambient Occlusion)는 틈새를 어둡게 만들어 입체감을 높입니다.

DOFPass(Depth of Field)는 특정 거리만 선명하고 나머지는 흐리게 만듭니다. FilmPass는 오래된 필름 같은 노이즈를 추가합니다.

각 Pass는 독립적으로 작동하므로 원하는 대로 조합할 수 있습니다. 하지만 너무 많이 추가하면 성능이 떨어집니다.

Pass 순서의 중요성 김개발 씨가 질문했습니다. "Pass 순서가 중요한가요?" "매우 중요합니다!" 박시니어 씨가 강조했습니다.

예를 들어 Bloom을 먼저 적용하고 Color Grading을 나중에 하면, Bloom의 밝은 부분도 색 보정이 됩니다. 순서를 바꾸면 결과가 완전히 달라집니다.

일반적으로 SSAO, Bloom 같은 효과를 먼저 하고, SMAA(안티앨리어싱) 같은 마무리 효과를 마지막에 추가합니다. 성능 최적화 PostProcessing은 화면 전체를 처리하므로 성능 영향이 큽니다.

특히 고해상도나 모바일에서는 신중해야 합니다. EffectComposer는 내부적으로 RenderTarget(오프스크린 버퍼)을 사용합니다.

해상도를 낮추면 성능이 향상되지만 화질이 떨어집니다. 적절한 균형점을 찾아야 합니다.

또한 일부 Pass는 선택적으로 활성화/비활성화할 수 있습니다. pass.enabled = false로 끄면 해당 Pass는 건너뜁니다.

실전 활용 사례 김개발 씨는 제품 쇼룸에 Bloom과 SSAO를 추가했습니다. 제품의 금속 부분이 은은하게 빛나고, 틈새가 자연스럽게 어두워져 훨씬 고급스러워 보였습니다.

클라이언트가 매우 만족했습니다. "이제야 프리미엄 느낌이 나네요!" 주의할 점 EffectComposer를 사용하면 renderer.render() 대신 composer.render()를 호출해야 합니다.

둘을 섞어 쓰면 안 됩니다. 또한 화면 크기가 바뀔 때(resize) composer.setSize()와 각 Pass의 setSize()도 함께 호출해야 합니다.

정리 박시니어 씨가 마무리했습니다. "PostProcessing은 3D 그래픽의 마지막 터치입니다.

적절히 사용하면 퀄리티가 한 단계 업그레이드됩니다." 김개발 씨는 다양한 효과를 실험하며 최적의 조합을 찾아갔습니다.

실전 팁

💡 - dat.GUI 같은 디버그 툴로 Pass 파라미터를 실시간으로 조정하며 최적값을 찾으세요

  • 모바일에서는 Pass 개수를 최소화하고, 해상도를 절반으로 줄여도 괜찮은 경우가 많습니다
  • 커스텀 Pass를 만들려면 ShaderPass를 사용해서 직접 Fragment Shader를 작성할 수 있습니다

5. 성능 프로파일링 및 최적화

김개발 씨의 3D 쇼룸이 거의 완성되었지만, 테스트 중 큰 문제가 발견되었습니다. 노트북에서는 부드럽게 돌아가는데, 모바일에서는 버벅거렸습니다.

"어디서 성능이 떨어지는 걸까?"

성능 프로파일링은 병목 지점을 찾아내는 과정입니다. Stats.js로 FPS를 모니터링하고, Chrome DevTools의 Performance 탭으로 CPU/GPU 사용량을 분석합니다.

Draw Calls, Triangles, Texture 메모리를 줄이는 것이 최적화의 핵심이며, Three.js의 Frustum Culling, LOD, Instancing 같은 기법을 활용합니다.

다음 코드를 살펴봅시다.

// 성능 모니터링 및 최적화 기본 설정
import Stats from 'three/examples/jsm/libs/stats.module.js';
import * as THREE from 'three';

// 1. Stats.js로 FPS 모니터링
const stats = new Stats();
stats.showPanel(0); // 0: FPS, 1: MS, 2: MB
document.body.appendChild(stats.dom);

// 2. Renderer 최적화 설정
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 고해상도 제한
renderer.powerPreference = 'high-performance'; // GPU 성능 모드

// 3. Geometry 최적화: 인스턴싱으로 동일 객체 대량 렌더링
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, 1000);

// 각 인스턴스 위치 설정 (1000개 객체를 1번의 Draw Call로)
for (let i = 0; i < 1000; i++) {
  const matrix = new THREE.Matrix4();
  matrix.setPosition(Math.random() * 100, Math.random() * 100, Math.random() * 100);
  instancedMesh.setMatrixAt(i, matrix);
}

// 4. Texture 최적화: 적절한 크기와 압축
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('texture.jpg');
texture.minFilter = THREE.LinearMipMapLinearFilter; // 밉맵 사용
texture.anisotropy = renderer.capabilities.getMaxAnisotropy(); // 최대 이방성 필터링

// 5. 애니메이션 루프 최적화
function animate() {
  stats.begin(); // 측정 시작

  composer.render(); // 또는 renderer.render()

  stats.end(); // 측정 종료
  requestAnimationFrame(animate);
}

박시니어 씨가 김개발 씨의 화면을 보며 한숨을 쉬었습니다. "FPS가 15까지 떨어지네요.

이건 사용자 경험이 좋지 않습니다." 김개발 씨는 당황했습니다. "어디서부터 고쳐야 할지 모르겠어요." 성능 문제의 원인 파악 3D 웹 애플리케이션이 느린 이유는 크게 세 가지입니다.

CPU 병목(JavaScript 연산), GPU 병목(렌더링), 메모리 병목(텍스처, 지오메트리)입니다. 먼저 어디가 문제인지 파악해야 합니다.

무작정 최적화하면 시간만 낭비합니다. 마치 의사가 진단 없이 약을 처방하지 않는 것과 같습니다.

Stats.js로 FPS 모니터링 박시니어 씨가 Stats.js를 추가했습니다. "가장 기본적인 도구입니다." Stats.js는 화면 왼쪽 위에 작은 그래프를 표시합니다.

FPS(초당 프레임), MS(프레임당 밀리초), MB(메모리 사용량)를 실시간으로 보여줍니다. FPS가 60이면 완벽, 30이면 약간 버벅임, 15 이하면 심각한 문제입니다.

목표는 항상 60 FPS를 유지하는 것입니다. Chrome DevTools Performance 탭 "더 자세히 보려면 Chrome DevTools를 사용하세요." 박시니어 씨가 F12를 눌렀습니다.

Performance 탭에서 녹화 버튼을 누르고 몇 초간 화면을 조작한 후 정지합니다. 그러면 CPU 사용량, GPU 활동, 메모리 할당이 시간 순서대로 표시됩니다.

CPU 사용률이 높으면 JavaScript 코드가 문제, GPU 사용률이 높으면 렌더링이 문제입니다. 어느 함수가 오래 걸리는지도 확인할 수 있습니다.

Draw Calls 줄이기 김개발 씨의 씬에는 수백 개의 작은 객체가 있었습니다. "각 객체마다 renderer가 GPU에 명령을 내립니다.

이게 Draw Call입니다." Draw Call이 많으면 CPU-GPU 통신 오버헤드가 커집니다. 같은 재질과 형태를 가진 객체들은 InstancedMesh로 합치면 1번의 Draw Call로 처리됩니다.

위 코드처럼 1000개 상자를 InstancedMesh로 만들면, 1000번의 Draw Call이 1번으로 줄어듭니다. 성능이 극적으로 향상됩니다.

Geometry 최적화 복잡한 3D 모델은 수십만 개의 삼각형(triangles)으로 구성됩니다. 삼각형이 많을수록 GPU 부담이 커집니다.

LOD(Level of Detail)를 사용하면 카메라와의 거리에 따라 다른 해상도의 모델을 표시합니다. 가까운 객체는 고해상도, 먼 객체는 저해상도로 렌더링하면 성능이 향상됩니다.

또한 보이지 않는 객체는 아예 렌더링하지 않습니다. Three.js는 자동으로 Frustum Culling(카메라 시야 밖 제거)을 하지만, 수동으로 object.visible = false를 설정할 수도 있습니다.

Texture 최적화 김개발 씨는 4K 해상도 텍스처를 사용하고 있었습니다. "모바일에서는 과도합니다." 텍스처 크기는 메모리와 렌더링 속도에 직접 영향을 줍니다.

대부분의 경우 1024x1024나 2048x2048면 충분합니다. 필요하다면 모바일에서는 더 작은 텍스처를 로드하도록 분기할 수 있습니다.

밉맵(Mipmap)을 사용하면 멀리 있는 객체에 저해상도 텍스처가 자동으로 적용됩니다. minFilter를 THREE.LinearMipMapLinearFilter로 설정하세요.

Material과 조명 최적화 MeshStandardMaterial은 물리 기반 렌더링(PBR)을 하므로 고품질이지만 무겁습니다. 간단한 객체는 MeshLambertMaterial이나 MeshPhongMaterial을 사용하세요.

조명도 성능에 영향을 줍니다. PointLightSpotLight는 그림자 계산이 비쌉니다.

개수를 최소화하고, 그림자가 꼭 필요한 조명만 castShadow를 활성화하세요. PostProcessing 최적화 PostProcessing Pass가 많으면 성능이 떨어집니다.

모바일에서는 1~2개만 사용하거나 아예 비활성화하는 것이 좋습니다. EffectComposer의 RenderTarget 해상도를 낮추면 성능이 향상됩니다.

composer.setSize(width * 0.5, height * 0.5) 같은 식으로 절반 크기로 렌더링할 수 있습니다. 실전 최적화 사례 김개발 씨는 다음과 같이 최적화했습니다.

동일한 제품 모델을 InstancedMesh로 변경, 텍스처를 1024x1024로 축소, 모바일에서는 PostProcessing 비활성화, 멀리 있는 객체는 LOD로 교체했습니다. 결과는 놀라웠습니다.

FPS가 15에서 55로 올라갔습니다! 주의할 점 성능 최적화는 측정 없이 하지 마세요.

"이게 느릴 것 같다"는 추측만으로 최적화하면 오히려 코드만 복잡해집니다. Stats.js와 DevTools로 병목을 확인한 후 집중적으로 개선하세요.

또한 최적화는 트레이드오프입니다. InstancedMesh는 빠르지만 각 객체를 개별 제어하기 어렵습니다.

상황에 맞게 선택하세요. 정리 박시니어 씨가 격려했습니다.

"성능 최적화는 끝이 없습니다. 하지만 핵심 원칙만 지키면 대부분의 문제는 해결됩니다." 김개발 씨는 이제 성능 문제가 생겨도 당황하지 않고 체계적으로 접근할 수 있게 되었습니다.

실전 팁

💡 - Spector.js 확장 프로그램을 사용하면 WebGL Draw Call을 프레임 단위로 분석할 수 있습니다

  • renderer.info 객체를 콘솔에 출력하면 현재 렌더링 통계(geometries, textures, triangles 등)를 확인할 수 있습니다
  • 복잡한 연산은 Web Worker로 분리해서 메인 스레드를 가볍게 유지하세요

6. 디버깅 도구 활용

김개발 씨는 버그를 만났습니다. 특정 각도에서만 모델이 사라지고, 조명이 이상하게 깜빡거렸습니다.

console.log로는 원인을 찾을 수 없었습니다. "3D 코드는 어떻게 디버깅하지?"

Three.js 디버깅은 시각적 도구를 활용하는 것이 핵심입니다. AxesHelper, GridHelper로 좌표계를 확인하고, CameraHelper, LightHelper로 카메라와 조명을 시각화합니다.

dat.GUI로 실시간 파라미터 조정이 가능하며, 브라우저 DevTools와 함께 사용하면 대부분의 문제를 빠르게 해결할 수 있습니다.

다음 코드를 살펴봅시다.

// Three.js 디버깅 도구 모음
import * as THREE from 'three';
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';

// 1. Axes와 Grid로 좌표계 시각화
const axesHelper = new THREE.AxesHelper(5); // 빨강=X, 초록=Y, 파랑=Z
scene.add(axesHelper);

const gridHelper = new THREE.GridHelper(10, 10); // 10x10 그리드
scene.add(gridHelper);

// 2. Camera 시각화 (다른 카메라로 볼 때)
const cameraHelper = new THREE.CameraHelper(camera);
scene.add(cameraHelper);

// 3. Light 시각화
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
const lightHelper = new THREE.DirectionalLightHelper(directionalLight, 5);
scene.add(lightHelper);

// 4. Bounding Box 시각화 (충돌 감지 디버깅)
const box = new THREE.Box3().setFromObject(mesh);
const boxHelper = new THREE.Box3Helper(box, 0xffff00);
scene.add(boxHelper);

// 5. dat.GUI로 실시간 파라미터 조정
const gui = new GUI();
gui.add(mesh.position, 'x', -10, 10).name('Position X');
gui.add(mesh.rotation, 'y', 0, Math.PI * 2).name('Rotation Y');
gui.add(material, 'metalness', 0, 1).name('Metalness');
gui.addColor({ color: 0xff0000 }, 'color').onChange((value) => {
  material.color.set(value);
});

// 6. 객체 정보 콘솔 출력
console.log('Renderer Info:', renderer.info);
console.log('Scene Graph:', scene.children);

박시니어 씨가 김개발 씨의 화면을 보며 웃었습니다. "3D 디버깅은 일반 코드 디버깅과 다릅니다.

눈으로 봐야 합니다." 김개발 씨가 고개를 갸우뚱했습니다. "눈으로 본다는 게 무슨 뜻이죠?" 시각적 디버깅의 중요성 3D 그래픽에서는 숫자만 봐서는 문제를 파악하기 어렵습니다.

position이 (5, 3, -2)라고 해서 그게 어디인지 직관적으로 알 수 없습니다. 따라서 Three.js는 다양한 Helper 객체를 제공합니다.

이것들은 보이지 않는 것들을 눈으로 볼 수 있게 시각화해줍니다. 마치 X-ray처럼 내부 구조를 드러냅니다.

좌표계 확인: AxesHelper와 GridHelper 박시니어 씨가 AxesHelper를 추가했습니다. "가장 기본적인 도구입니다." AxesHelper는 원점에서 세 개의 화살표를 그립니다.

빨강은 X축, 초록은 Y축, 파랑은 Z축입니다. 이것으로 어느 방향이 어느 축인지 한눈에 알 수 있습니다.

GridHelper는 바닥에 격자를 그립니다. 마치 그래프 용지처럼 공간감을 파악하기 쉬워집니다.

객체가 공중에 떠 있는지, 바닥에 붙어 있는지도 명확해집니다. 카메라 시각화: CameraHelper "카메라가 어디를 보고 있는지 확인하려면 CameraHelper를 쓰세요." CameraHelper는 카메라의 Frustum(시야 범위)을 선으로 그려줍니다.

특정 객체가 카메라 시야에 들어오는지 확인할 때 유용합니다. 단, CameraHelper는 다른 카메라로 봐야 보입니다.

같은 카메라로는 자기 자신을 볼 수 없습니다. 디버그용 카메라를 하나 더 만들거나, OrbitControls로 시점을 자유롭게 움직이며 확인하세요.

조명 시각화: LightHelper 김개발 씨의 버그는 조명 때문이었습니다. DirectionalLight의 방향이 잘못 설정되어 있었습니다.

DirectionalLightHelper를 추가하자 조명의 위치와 방향이 선으로 표시되었습니다. "아, 조명이 반대쪽을 향하고 있었네요!" PointLight는 PointLightHelper, SpotLight는 SpotLightHelper를 사용하세요.

각 조명의 특성에 맞게 시각화됩니다. Bounding Box와 충돌 감지 객체의 크기와 위치를 확인하려면 Box3Helper를 사용합니다.

Box3.setFromObject(mesh)로 객체를 감싸는 최소 사각형(Bounding Box)을 계산하고, Box3Helper로 그것을 시각화합니다. 충돌 감지 로직을 디버깅할 때 필수입니다.

실시간 파라미터 조정: dat.GUI 박시니어 씨가 dat.GUI를 추가했습니다. 화면 오른쪽에 슬라이더와 컬러 피커가 나타났습니다.

"이제 코드를 수정하지 않고도 값을 바꿔볼 수 있습니다." 슬라이더를 움직이자 객체가 실시간으로 이동했습니다. dat.GUI는 숫자, 색상, 불리언, 드롭다운 등 다양한 입력을 지원합니다.

최적의 값을 찾을 때, 또는 클라이언트에게 여러 옵션을 보여줄 때 매우 유용합니다. console.log의 활용 3D 코드에서도 console.log는 여전히 유용합니다.

특히 renderer.info 객체는 렌더링 통계를 담고 있어 성능 디버깅에 도움이 됩니다. scene.children를 출력하면 씬에 어떤 객체가 있는지 확인할 수 있습니다.

traverse를 사용해서 특정 조건의 객체를 찾을 수도 있습니다. 브라우저 DevTools 활용 Chrome DevTools의 3D View는 DOM을 3D로 시각화하지만, Three.js 씬은 지원하지 않습니다.

대신 Performance 탭과 Memory 탭을 활용하세요. Performance 탭에서는 requestAnimationFrame의 호출 주기를 확인할 수 있습니다.

60 FPS를 유지하려면 16.67ms 이내에 프레임이 완료되어야 합니다. 실전 디버깅 사례 김개발 씨는 모델이 사라지는 버그를 추적했습니다.

AxesHelper로 좌표를 확인하니 카메라 far 값이 너무 작아서 멀리 있는 객체가 잘렸습니다. 조명 깜빡임은 DirectionalLightHelper로 확인하니 조명이 애니메이션 루프에서 계속 추가되고 있었습니다.

init 함수로 옮기니 해결되었습니다. 디버그 모드 전환 실제 프로덕션에서는 Helper 객체들을 보여주면 안 됩니다.

DEBUG 플래그를 만들어서 개발 중에만 활성화하세요. ```javascript const DEBUG = true; if (DEBUG) { scene.add(axesHelper, gridHelper); const gui = new GUI(); // ...

} ``` 주의할 점 Helper 객체도 렌더링 비용이 있습니다. 너무 많이 추가하면 성능에 영향을 줄 수 있습니다.

또한 dat.GUI는 실무 프로젝트에서는 제거하고, 내부 테스트나 클라이언트 리뷰용으로만 사용하세요. 정리 박시니어 씨가 마무리했습니다.

"디버깅 도구를 적극 활용하세요. 추측하지 말고 확인하는 것이 버그를 빠르게 찾는 비결입니다." 김개발 씨는 이제 복잡한 3D 버그도 자신 있게 해결할 수 있게 되었습니다.

실전 팁

💡 - Three.js Inspector(브라우저 확장)를 설치하면 DevTools에 Three.js 탭이 추가되어 씬 그래프를 트리 형태로 볼 수 있습니다

  • Raycaster로 마우스 클릭한 객체를 console.log하면 씬에서 원하는 객체를 빠르게 찾을 수 있습니다
  • 개발 중에는 renderer.shadowMap.enabled를 껐다 켜며 그림자 문제인지 확인하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Three.js#Shader#PostProcessing#Performance#Debugging#Three.js,Blender,3D웹개발

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스