Skip to main content
민트라

3D 포트폴리오 웹사이트 제작기 - 기획부터 배포까지

취업 준비를 하면서 수많은 포트폴리오 사이트를 봤습니다. 대부분 비슷한 레이아웃에 비슷한 구성이었죠. "어떻게 하면 눈에 띌 수 있을까?" 고민하다가 3D 웹사이트를 만들기로 결심했습니다. 이 글은 그 과정에서 겪은 시행착오를 솔직하게 공유합니다.

왜 3D 포트폴리오였나 #

2D 포트폴리오의 한계를 느꼈습니다. 스크롤하면서 프로젝트 카드를 보는 것은 지루했고, 방문자가 10초 만에 이탈한다는 통계를 봤습니다. 3D라면 최소한 "뭐지?" 하고 멈춰서 볼 것 같았습니다.

물론 위험 부담도 있었습니다. 로딩이 오래 걸리면 오히려 역효과가 날 수 있고, 모바일에서 제대로 안 돌아갈 수도 있었습니다. 하지만 프론트엔드 개발자로서 기술력을 보여주기에는 좋은 선택이라고 생각했습니다.

첫 번째 시도: 너무 욕심을 부렸다 #

처음에는 Awwwards에서 본 사이트들처럼 만들고 싶었습니다. 파티클이 날아다니고, 3D 텍스트가 화면을 채우고, 마우스를 따라 모든 것이 반응하는 그런 사이트요.

// 첫 번째 버전 - 이렇게 시작했습니다
const particleCount = 50000;
const particles = new THREE.Points(geometry, material);

// 매 프레임마다 5만 개 파티클 업데이트...
function animate() {
  for (let i = 0; i < particleCount; i++) {
    // 위치 계산
    // 색상 계산
    // 크기 계산
  }
}

결과는 참담했습니다. 제 맥북에서는 40fps가 나왔지만, 테스트용 저사양 노트북에서는 15fps도 안 나왔습니다. 모바일은 말할 것도 없었고요.

현실과 타협하기 #

성능 문제를 해결하려면 과감히 덜어내야 했습니다.

버린 것들:

추가한 것들:

// 디바이스별 품질 설정
function getQualitySettings() {
  const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
  const memory = navigator.deviceMemory || 4;

  if (isMobile || memory < 4) {
    return {
      particleCount: 1000,
      shadowMapSize: 512,
      antialias: false,
      pixelRatio: 1
    };
  }

  return {
    particleCount: 3000,
    shadowMapSize: 1024,
    antialias: true,
    pixelRatio: Math.min(window.devicePixelRatio, 2)
  };
}

가장 어려웠던 부분: 스크롤 연동 #

3D 씬이 스크롤에 따라 변화하는 것을 원했습니다. 섹션 1에서는 카메라가 멀리서 보고, 섹션 2에서는 가까이 다가가고, 섹션 3에서는 다른 각도로 보는 식으로요.

처음에는 scroll 이벤트를 직접 썼는데, 버벅거림이 심했습니다.

// 잘못된 방법
window.addEventListener('scroll', () => {
  const progress = window.scrollY / document.body.scrollHeight;
  camera.position.z = 10 - progress * 5; // 이 부분에서 jank 발생
});

GSAP ScrollTrigger로 바꾸니 훨씬 부드러워졌습니다.

// 개선된 방법
gsap.to(camera.position, {
  z: 5,
  scrollTrigger: {
    trigger: '#section-2',
    start: 'top center',
    end: 'bottom center',
    scrub: 1  // 1초 딜레이로 부드럽게
  }
});

scrub: true 대신 scrub: 1을 쓴 것이 핵심이었습니다. 약간의 지연이 오히려 자연스러워 보였습니다.

모바일 대응의 고통 #

"일단 데스크톱부터 만들고 모바일은 나중에"라고 생각했던 것이 실수였습니다. 거의 다 만들고 나서 모바일을 테스트했는데, 문제가 산더미였습니다.

문제 1: 터치 이벤트

OrbitControls가 모바일에서 이상하게 작동했습니다. 스크롤하려고 터치하면 3D 씬이 회전해버렸죠.

// 해결책: 스크롤과 3D 인터랙션 분리
controls.enabled = false;  // 기본적으로 비활성화

// 특정 영역에서만 3D 컨트롤 활성화
const interactiveArea = document.querySelector('#3d-interactive');
interactiveArea.addEventListener('touchstart', () => {
  controls.enabled = true;
});

문제 2: 주소창 높이 변화

모바일 브라우저에서 스크롤할 때 주소창이 숨겨지면서 뷰포트 높이가 변합니다. 이때 3D 씬이 갑자기 리사이즈되면서 덜컹거렸습니다.

// 해결책: 초기 높이 고정
const initialHeight = window.innerHeight;

window.addEventListener('resize', () => {
  // 너비 변화만 처리, 높이는 무시
  if (window.innerWidth !== previousWidth) {
    handleResize();
  }
});

문제 3: 발열

3D 렌더링이 계속 돌아가니 폰이 뜨거워졌습니다. 배터리도 금방 닳았고요.

// 해결책: 화면에 보일 때만 렌더링
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      startRendering();
    } else {
      stopRendering();
    }
  });
});

observer.observe(canvasElement);

로딩 시간 줄이기 #

초기 로딩이 8초나 걸렸습니다. 3D 모델, 텍스처, 환경맵 등을 한꺼번에 불러왔기 때문입니다.

시도한 것들:

  1. Draco 압축: 3D 모델 용량 70% 감소
  2. 텍스처 압축: PNG → WebP, 필요한 경우 KTX2
  3. 지연 로딩: 스크롤 위치에 따라 필요한 에셋만 로드
// 섹션별 지연 로딩
const sections = {
  intro: ['model-hero.glb', 'env-studio.hdr'],
  projects: ['model-laptop.glb', 'screenshots/*.webp'],
  contact: ['model-phone.glb']
};

async function loadSection(name) {
  if (loadedSections.has(name)) return;

  const assets = sections[name];
  await Promise.all(assets.map(loadAsset));
  loadedSections.add(name);
}

// 스크롤 위치에 따라 다음 섹션 미리 로드
ScrollTrigger.create({
  trigger: '#projects',
  start: 'top bottom',
  onEnter: () => loadSection('projects')
});

최종적으로 초기 로딩을 3초로 줄였습니다.

배운 점들 #

1. 성능부터 생각하기

멋진 효과를 넣고 나중에 최적화하는 것보다, 처음부터 성능 예산을 정하고 그 안에서 만드는 것이 효율적입니다.

2. 모바일 퍼스트

3D 웹에서도 모바일 퍼스트가 유효합니다. 저사양에서 잘 돌아가면 고사양은 자연히 됩니다.

3. 폴백 준비

WebGL을 지원하지 않는 환경, 저사양 기기, 배터리 절약 모드 등 다양한 상황에 대비해야 합니다.

// WebGL 미지원 시 정적 이미지로 폴백
if (!WebGLRenderingContext) {
  document.querySelector('#canvas').style.display = 'none';
  document.querySelector('#fallback-image').style.display = 'block';
}

4. 콘텐츠가 먼저

3D가 화려해도 결국 사람들이 보는 것은 프로젝트 내용입니다. 기술에 취해서 정작 중요한 콘텐츠를 소홀히 하면 안 됩니다.

결과 #

완성된 포트폴리오로 면접 기회를 여러 번 얻었고, 면접에서도 "포트폴리오 직접 만드셨어요?"라는 질문을 자주 받았습니다. 기술적인 대화로 이어지기 좋았습니다.

Google Analytics를 보니 평균 체류 시간이 2분 30초였습니다. 일반적인 포트폴리오 사이트의 평균(30초~1분)보다 훨씬 길었죠. 3D라서 구경하느라 더 머물렀던 것 같습니다.

물론 이 모든 과정이 3개월이나 걸렸습니다. 빠르게 취업해야 하는 상황이라면 추천하지 않습니다. 하지만 시간적 여유가 있고, 프론트엔드 기술력을 어필하고 싶다면 도전해 볼 만합니다.

다음 글에서는 이 포트폴리오에서 가장 고생했던 WebGL 메모리 누수 디버깅에 대해 이야기하겠습니다.