본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 29. · 3 Views
3D 포트폴리오 웹사이트 개발 완벽 가이드
Three.js와 Blender를 활용하여 인상적인 3D 포트폴리오 웹사이트를 만드는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 중심으로 설명합니다. 프로젝트 구조부터 접근성까지 모든 과정을 담았습니다.
목차
- 프로젝트 구조 설계
- 로딩 화면 구현
- 반응형 레이아웃 적용
- 모바일 터치 컨트롤 추가
- 테마 변경(Theme Switcher) 구현
- UI/UX 통합 및 인터랙션
- 접근성(Accessibility) 고려
1. 프로젝트 구조 설계
신입 개발자 이준호 씨는 3D 포트폴리오 사이트를 만들어 보기로 했습니다. 하지만 막상 시작하려니 어디서부터 손을 대야 할지 막막했습니다.
파일을 어떻게 나눠야 할까요? 3D 모델은 어디에 두어야 할까요?
선배 개발자 최수진 씨가 말합니다. "프로젝트 구조부터 제대로 잡아야 나중에 고생하지 않아요."
프로젝트 구조 설계는 3D 웹 애플리케이션의 뼈대를 만드는 첫 단계입니다. 마치 집을 지을 때 설계도를 먼저 그리는 것처럼, 코드와 리소스를 어떻게 배치할지 미리 계획해야 합니다.
올바른 구조는 유지보수를 쉽게 만들고, 협업할 때도 큰 도움이 됩니다.
다음 코드를 살펴봅시다.
// 프로젝트 폴더 구조
project-root/
├── src/
│ ├── components/ // 재사용 가능한 3D 컴포넌트
│ │ ├── Scene.js
│ │ ├── Camera.js
│ │ └── Lights.js
│ ├── models/ // 3D 모델 파일 (.glb, .gltf)
│ ├── textures/ // 텍스처 이미지
│ ├── shaders/ // 커스텀 셰이더
│ ├── utils/ // 헬퍼 함수들
│ └── main.js // 앱 진입점
├── public/
│ └── index.html
└── package.json
이준호 씨는 유튜브에서 멋진 3D 포트폴리오 사이트를 봤습니다. 화면을 스크롤하면 3D 모델이 빙글빙글 돌아가고, 마우스를 따라 카메라가 움직이는 모습이 너무나 인상적이었습니다.
"나도 이런 걸 만들 수 있을까?" 다음 날 회사에서 점심시간, 이준호 씨는 선배 최수진 씨에게 조심스럽게 물어봤습니다. "저도 3D 포트폴리오 사이트를 만들어보고 싶은데, 어떻게 시작해야 할까요?" 최수진 씨는 웃으며 대답했습니다.
"좋은 도전이네요! 그런데 말이죠, 3D 프로젝트는 일반 웹 프로젝트보다 파일이 훨씬 많아요.
모델 파일, 텍스처 이미지, 셰이더 코드까지... 정리 안 하면 나중에 정말 난감해집니다." 프로젝트 구조란 무엇일까요?
쉽게 비유하자면, 프로젝트 구조는 마치 도서관의 분류 체계와 같습니다. 소설, 에세이, 전문서적을 각각의 서가에 정리해 두면 나중에 찾기 쉽습니다.
마찬가지로 3D 모델은 models 폴더에, 텍스처는 textures 폴더에 넣어두면 개발할 때 헤매지 않습니다. 특히 3D 웹 프로젝트는 파일 종류가 다양합니다.
JavaScript 코드뿐만 아니라 GLTF 모델 파일, PNG/JPG 텍스처, GLSL 셰이더 파일까지 모두 관리해야 합니다. 구조화가 없던 시절에는 어땠을까요?
초창기 개발자들은 모든 파일을 한 폴더에 때려 넣었습니다. model1.glb, texture1.png, main.js, helper.js...
프로젝트가 커지면 파일이 수백 개가 되었고, 원하는 파일을 찾는 데만 10분씩 걸렸습니다. 더 큰 문제는 협업이었습니다.
다른 개발자가 합류하면 "이 파일이 뭐죠?"라는 질문을 수도 없이 받았습니다. 코드 리뷰도 어려웠습니다.
파일 하나를 수정하면 연관된 다른 파일들을 찾기가 너무 힘들었으니까요. 바로 이런 문제를 해결하기 위해 체계적인 폴더 구조가 등장했습니다.
먼저 src 폴더는 모든 소스 코드의 집입니다. 여기에 components, models, textures, shaders, utils 같은 하위 폴더를 만듭니다.
components 폴더에는 재사용 가능한 코드 조각들을 넣습니다. Scene.js는 3D 장면을 설정하고, Camera.js는 카메라 설정을, Lights.js는 조명 설정을 담당합니다.
이렇게 나누면 각 파일이 하나의 책임만 갖게 됩니다. models 폴더에는 Blender나 다른 3D 툴에서 만든 모델 파일들을 보관합니다.
주로 GLB나 GLTF 형식을 사용합니다. textures 폴더에는 모델에 입힐 이미지 파일들을 넣습니다.
shaders 폴더는 조금 특별합니다. 여기에는 GLSL로 작성된 셰이더 코드를 저장합니다.
셰이더는 GPU에서 실행되는 특수한 프로그램인데, 물, 불, 금속 같은 특수 효과를 만들 때 사용합니다. utils 폴더에는 여기저기서 쓰이는 헬퍼 함수들을 모아둡니다.
예를 들어 모델 로딩 함수, 수학 계산 함수 같은 것들입니다. 위의 폴더 구조를 보면 전체 프로젝트가 한눈에 들어옵니다.
루트에 src와 public이 있고, src 안에 역할별로 폴더가 나뉘어 있습니다. main.js가 앱의 시작점이 되어 필요한 컴포넌트들을 불러옵니다.
마치 오케스트라 지휘자가 각 악기 파트를 조화롭게 이끄는 것처럼요. 실무에서는 어떻게 활용할까요? 실제 회사에서 3D 웹 프로젝트를 진행한다고 가정해봅시다.
디자이너가 새로운 3D 모델을 전달하면, 개발자는 바로 models 폴더에 넣습니다. 텍스처 아티스트가 이미지를 보내면 textures 폴더에 추가합니다.
코드 리뷰를 할 때도 편합니다. "Scene.js의 25번째 줄을 보세요"라고 말하면 모두가 같은 파일을 보고 있습니다.
파일을 찾느라 시간을 낭비하지 않습니다. 유지보수도 훨씬 쉬워집니다.
6개월 후에 조명을 수정해야 한다면? Lights.js 파일만 열면 됩니다.
새로운 팀원이 합류해도 폴더 구조만 보면 프로젝트를 빠르게 파악할 수 있습니다. 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 세분화하는 것입니다.
폴더를 10단계, 20단계로 깊게 만들면 오히려 불편해집니다. 파일 하나 찾으려고 폴더를 여러 번 클릭해야 하니까요.
또 다른 실수는 일관성이 없는 것입니다. 어떤 컴포넌트는 components에, 어떤 것은 src 바로 밑에 두면 나중에 혼란스럽습니다.
한번 정한 규칙은 끝까지 지켜야 합니다. 다시 이준호 씨의 이야기로 돌아가봅시다. 최수진 씨의 설명을 들은 이준호 씨는 고개를 끄덕였습니다.
"아, 그래서 구조가 중요하군요!" 그날 저녁, 이준호 씨는 새 프로젝트를 만들고 폴더부터 차근차근 정리했습니다. 프로젝트 구조를 제대로 설계하면 나중에 시간을 엄청나게 아낄 수 있습니다.
처음에는 번거롭게 느껴지지만, 프로젝트가 커질수록 그 가치를 실감하게 됩니다. 여러분도 새 프로젝트를 시작할 때 5분만 투자해서 폴더 구조를 먼저 설계해 보세요.
실전 팁
💡 - 폴더 깊이는 3단계를 넘지 않도록 설계하세요
- 파일명은 PascalCase나 camelCase로 일관되게 작성하세요
- README.md에 폴더 구조와 각 폴더의 역할을 문서화해두면 협업할 때 큰 도움이 됩니다
2. 로딩 화면 구현
이준호 씨가 처음 만든 3D 사이트를 친구에게 보여줬더니 화면이 한참 동안 하얗게만 떠 있었습니다. "고장 난 거 아니야?" 친구가 묻자 이준호 씨는 당황했습니다.
3D 모델이 로딩되는 동안 아무것도 보이지 않았던 겁니다. 최수진 씨가 말합니다.
"로딩 화면을 만들어야죠. 사용자가 기다릴 수 있게요."
로딩 화면은 3D 모델이나 텍스처 같은 무거운 리소스를 불러오는 동안 사용자에게 진행 상황을 보여주는 기능입니다. 마치 커피숍에서 "주문하신 음료 나오고 있습니다"라고 알려주는 것처럼, 사용자에게 "지금 열심히 준비 중이에요"라고 말해주는 역할을 합니다.
로딩 화면이 있으면 사용자 경험이 크게 개선됩니다.
다음 코드를 살펴봅시다.
// LoadingManager로 로딩 진행률 추적
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
const loadingManager = new THREE.LoadingManager();
const progressBar = document.getElementById('progress-bar');
const loadingScreen = document.getElementById('loading-screen');
// 로딩 진행 시 호출되는 콜백
loadingManager.onProgress = (url, loaded, total) => {
const progress = (loaded / total) * 100;
progressBar.style.width = progress + '%';
console.log(`로딩 중: ${progress.toFixed(2)}%`);
};
// 모든 리소스 로딩 완료 시
loadingManager.onLoad = () => {
loadingScreen.style.display = 'none';
console.log('로딩 완료!');
};
const gltfLoader = new GLTFLoader(loadingManager);
gltfLoader.load('/models/portfolio.glb', (gltf) => {
scene.add(gltf.scene);
});
어느 날 이준호 씨는 자랑스럽게 첫 3D 포트폴리오 사이트를 친구들에게 공유했습니다. 링크를 클릭한 친구들이 하나둘 반응을 보내왔습니다.
"아무것도 안 보이는데?" "내 컴퓨터가 느린가?" "깨진 거 같은데?" 이준호 씨는 황급히 자기 브라우저로 확인해봤습니다. 처음 페이지를 열면 5초 정도 흰 화면만 보이다가 갑자기 3D 장면이 나타났습니다.
"아, 로딩 시간 동안 사용자가 아무것도 못 보는구나!" 다음 날 회사에서 최수진 씨에게 고민을 털어놨습니다. 최수진 씨는 웃으며 말했습니다.
"3D 파일은 용량이 크잖아요. 10MB, 20MB는 기본이고요.
로딩 화면 없이는 사용자가 떠나버릴 수 있어요." 로딩 화면이란 무엇일까요? 쉽게 비유하자면, 로딩 화면은 마치 레스토랑의 웨이터와 같습니다.
주방에서 요리가 준비되는 동안 웨이터가 "5분 정도 소요됩니다"라고 알려주면 손님은 안심하고 기다립니다. 하지만 아무 말이 없으면 손님은 불안해하며 "주문이 들어갔나?" 하고 의심하게 됩니다.
3D 웹사이트도 마찬가지입니다. 모델 파일이 네트워크를 통해 다운로드되고, GPU가 텍스처를 처리하는 동안 사용자에게 "지금 열심히 준비 중이에요"라고 알려줘야 합니다.
로딩 화면이 없던 시절에는 어땠을까요? 초창기 3D 웹사이트들은 로딩 중에 아무것도 보여주지 않았습니다.
사용자는 흰 화면이나 검은 화면만 보며 멍하니 기다려야 했습니다. 인터넷이 느린 환경에서는 1분 이상 걸리기도 했습니다.
사용자들은 대부분 "사이트가 고장 났나?"라고 생각하고 떠나버렸습니다. 실제로 통계를 보면 로딩 시간이 3초를 넘으면 이탈률이 급격히 높아집니다.
멋진 3D 작품을 만들어도 사용자가 보기도 전에 떠나버린다면 무슨 소용이 있을까요? 바로 이런 문제를 해결하기 위해 로딩 화면과 진행률 표시가 등장했습니다.
Three.js는 LoadingManager라는 강력한 도구를 제공합니다. 이것을 사용하면 모든 리소스의 로딩 상태를 한 곳에서 관리할 수 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 LoadingManager 인스턴스를 생성합니다.
이것은 모든 로더들을 감시하는 관리자 역할을 합니다. 그다음 HTML에서 프로그레스 바와 로딩 스크린 요소를 가져옵니다.
onProgress 콜백은 리소스가 로딩될 때마다 호출됩니다. loaded는 현재까지 로딩된 아이템 수, total은 전체 아이템 수입니다.
이 둘을 나누면 진행률 퍼센트를 계산할 수 있습니다. 프로그레스 바의 너비를 이 값에 맞춰 조정하면 시각적으로 진행 상황이 표시됩니다.
onLoad 콜백은 모든 리소스가 완전히 로딩되었을 때 한 번 호출됩니다. 이때 로딩 스크린을 숨기고 실제 3D 장면을 보여줍니다.
마지막으로 GLTFLoader에 loadingManager를 전달합니다. 이제 이 로더가 파일을 불러올 때마다 LoadingManager가 자동으로 추적합니다.
실무에서는 어떻게 활용할까요? 실제 포트폴리오 사이트를 만든다고 가정해봅시다. 메인 캐릭터 모델이 15MB, 배경 환경 모델이 10MB, 각종 텍스처가 합쳐서 20MB라면 총 45MB의 데이터를 다운로드해야 합니다.
4G 모바일 환경에서는 이게 10초 이상 걸릴 수 있습니다. 이때 로딩 화면을 보여주면 사용자는 "아, 지금 45% 로딩됐네.
조금만 기다리면 되겠구나"라고 생각합니다. 많은 유명 3D 웹사이트들이 창의적인 로딩 화면을 사용합니다.
애니메이션을 추가하거나, 프로젝트 소개 문구를 보여주거나, 재미있는 팁을 표시하기도 합니다. 로딩 시간을 지루한 대기가 아닌 몰입의 시작으로 만드는 거죠.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 진행률이 부정확한 것입니다. 예를 들어 99%에서 오래 멈춰 있으면 사용자는 더 짜증이 납니다.
LoadingManager를 사용하면 자동으로 정확한 진행률을 계산하므로 이런 문제를 피할 수 있습니다. 또 다른 실수는 로딩 화면이 너무 화려해서 오히려 더 무거워지는 경우입니다.
로딩 화면 자체가 5MB짜리 동영상이면 본말이 전도된 거죠. 로딩 화면은 가볍고 빠르게 표시되어야 합니다.
다시 이준호 씨의 이야기로 돌아가봅시다. 최수진 씨의 조언을 듣고 이준호 씨는 로딩 화면을 추가했습니다. 간단한 프로그레스 바와 "Loading..." 텍스트만 넣었는데도 사용자 반응이 완전히 달라졌습니다.
"멋진 사이트네!"라는 칭찬이 들어오기 시작했습니다. 로딩 화면을 제대로 구현하면 기술적으로는 아무것도 바뀌지 않았는데 사용자 경험이 크게 개선됩니다.
단 몇 줄의 코드로 사이트의 완성도를 한 단계 높일 수 있습니다. 여러분도 3D 프로젝트를 만들 때 로딩 화면을 꼭 추가해 보세요.
실전 팁
💡 - HTML에서 로딩 스크린은 position: fixed로 전체 화면을 덮도록 만드세요
- 프로그레스 바에 transition 효과를 주면 부드럽게 움직입니다
- onError 콜백도 추가해서 로딩 실패 시 사용자에게 알려주세요
3. 반응형 레이아웃 적용
이준호 씨의 포트폴리오 사이트가 점점 모양을 갖춰갔습니다. 데스크톱에서는 완벽하게 보였습니다.
하지만 친구가 스마트폰으로 접속하더니 말했습니다. "화면이 짤려서 보여." 이준호 씨는 깜짝 놀랐습니다.
모바일에서는 레이아웃이 완전히 망가져 있었던 겁니다.
반응형 레이아웃은 다양한 화면 크기에서도 3D 장면이 올바르게 보이도록 만드는 기술입니다. 마치 물이 그릇 모양에 따라 변하듯이, 웹사이트도 데스크톱, 태블릿, 모바일 화면에 맞춰 자동으로 조정되어야 합니다.
카메라 비율과 렌더러 크기를 동적으로 업데이트하는 것이 핵심입니다.
다음 코드를 살펴봅시다.
// 반응형 리사이즈 처리
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight, // 화면 비율
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 윈도우 리사이즈 이벤트 리스너
window.addEventListener('resize', () => {
// 카메라 종횡비 업데이트
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
// 렌더러 크기 업데이트
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
이준호 씨는 주말 내내 3D 포트폴리오 사이트를 다듬었습니다. 27인치 모니터에서 보니 정말 멋졌습니다.
3D 모델이 화면 가득 채워지고, 마우스를 움직이면 부드럽게 반응했습니다. "완벽해!" 월요일 아침, 출근길 지하철에서 친구에게 자랑스럽게 링크를 보냈습니다.
잠시 후 친구에게서 답장이 왔습니다. "이상한데?
3D 모델이 눌려 보여. 화면 오른쪽 절반이 잘렸어." 회사에 도착하자마자 이준호 씨는 자기 폰으로 사이트를 열어봤습니다.
충격이었습니다. 데스크톱에서는 완벽하던 레이아웃이 모바일에서는 완전히 망가져 있었습니다.
카메라 비율이 이상해서 3D 모델이 납작하게 찌그러져 보였고, 일부 요소는 화면 밖으로 튀어나가 있었습니다. 점심시간에 최수진 씨에게 SOS를 청했습니다.
최수진 씨는 화면을 보더니 바로 문제를 짚어냈습니다. "반응형 처리를 안 했네요.
3D는 일반 웹페이지보다 신경 쓸 게 더 많아요." 반응형 레이아웃이란 무엇일까요? 쉽게 비유하자면, 반응형 레이아웃은 마치 신축성 있는 옷과 같습니다.
같은 티셔츠를 어린이가 입어도, 어른이 입어도 몸에 맞게 늘어나는 것처럼, 웹사이트도 스마트폰, 태블릿, 데스크톱에서 각각 화면에 맞게 조정되어야 합니다. 일반 웹페이지는 CSS 미디어 쿼리로 반응형을 처리하지만, 3D는 조금 다릅니다.
카메라의 종횡비와 렌더러의 해상도를 JavaScript로 직접 조정해야 합니다. 반응형 처리를 안 하던 시절에는 어땠을까요?
예전에는 웹사이트를 데스크톱 기준으로만 만들었습니다. "모바일로 보는 사람은 어차피 적을 거야"라고 생각했죠.
하지만 지금은 전체 웹 트래픽의 60% 이상이 모바일에서 발생합니다. 3D 사이트도 마찬가지입니다.
멋진 포트폴리오를 만들어도 면접관이 스마트폰으로 보는데 화면이 깨져 있다면? 첫인상이 최악이 됩니다.
기술력을 보여주려고 만든 사이트가 오히려 "기본도 모르는 개발자"라는 인상을 줄 수 있습니다. 바로 이런 문제를 해결하기 위해 반응형 3D 레이아웃이 필수가 되었습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 PerspectiveCamera를 생성할 때 두 번째 매개변수로 종횡비를 전달합니다.
window.innerWidth를 window.innerHeight로 나눈 값이 바로 화면의 가로세로 비율입니다. 16:9 모니터면 1.78 정도, 스마트폰을 세로로 들면 0.56 정도의 값이 나옵니다.
renderer.setSize는 렌더링할 캔버스 크기를 설정합니다. 전체 화면을 채우려면 window 크기를 그대로 사용하면 됩니다.
setPixelRatio는 고해상도 디스플레이를 위한 설정입니다. Retina 디스플레이처럼 픽셀 밀도가 높은 화면에서는 devicePixelRatio가 2 이상입니다.
하지만 너무 높은 값을 사용하면 성능이 떨어지므로 최대 2로 제한합니다. 가장 중요한 부분은 resize 이벤트 리스너입니다.
사용자가 브라우저 창 크기를 바꾸거나, 모바일에서 가로세로 회전을 하면 이 이벤트가 발생합니다. 이벤트 핸들러 안에서 camera.aspect를 새로운 비율로 업데이트하고, updateProjectionMatrix를 호출해서 카메라에 변경사항을 적용합니다.
그다음 renderer의 크기와 픽셀 비율도 다시 설정합니다. 실무에서는 어떻게 활용할까요? 실제로 포트폴리오 사이트를 배포한다고 가정해봅시다.
채용 담당자가 회사 컴퓨터로 볼 수도 있고, 출퇴근길에 스마트폰으로 볼 수도 있습니다. 회의실에서 태블릿으로 보여줄 수도 있습니다.
모든 디바이스에서 완벽하게 보이려면 반응형 처리가 필수입니다. 특히 3D 모델의 경우 종횡비가 틀어지면 왜곡되어 보이므로 더욱 신경 써야 합니다.
많은 3D 웹사이트들이 추가로 브레이크포인트를 설정합니다. 예를 들어 모바일에서는 카메라를 조금 더 뒤로 빼서 전체 모델이 보이도록 하고, 데스크톱에서는 가까이에서 디테일을 보여주는 식입니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 resize 이벤트가 너무 자주 호출되는 것을 고려하지 않는 것입니다. 창 크기를 조금씩 드래그하면 초당 수십 번 이벤트가 발생합니다.
무거운 연산을 넣으면 성능이 떨어질 수 있습니다. 이럴 때는 디바운싱이나 쓰로틀링을 적용하면 좋습니다.
하지만 카메라와 렌더러 업데이트는 워낙 빠르게 실행되므로 대부분의 경우 그냥 써도 괜찮습니다. 또 다른 실수는 updateProjectionMatrix를 빼먹는 것입니다.
camera.aspect만 바꾸고 이 메서드를 호출하지 않으면 변경사항이 적용되지 않습니다. 화면은 여전히 왜곡되어 보입니다.
다시 이준호 씨의 이야기로 돌아가봅시다. 최수진 씨가 알려준 코드를 추가한 이준호 씨는 다시 테스트해봤습니다. 이번에는 모바일에서도 완벽했습니다.
폰을 가로로 돌리면 자동으로 레이아웃이 조정되었고, 태블릿에서도 아름답게 보였습니다. 반응형 레이아웃을 구현하면 모든 사용자에게 일관된 경험을 제공할 수 있습니다.
단 몇 줄의 코드로 프로페셔널한 인상을 줄 수 있습니다. 여러분의 3D 프로젝트에도 꼭 추가해 보세요.
실전 팁
💡 - 테스트할 때는 브라우저 개발자 도구의 디바이스 시뮬레이터를 활용하세요
- 극단적인 비율(초광각 모니터, 접이식 폰 등)도 테스트해보면 좋습니다
- 성능이 중요하다면 resize 이벤트에 requestAnimationFrame을 결합하세요
4. 모바일 터치 컨트롤 추가
반응형 레이아웃까지 완성한 이준호 씨는 뿌듯했습니다. 하지만 엄마에게 사이트를 보여줬더니 예상 밖의 반응이 나왔습니다.
"화면을 터치해도 아무 일도 안 일어나는데?" 데스크톱의 마우스 드래그는 완벽한데, 모바일 터치는 전혀 작동하지 않았던 겁니다.
모바일 터치 컨트롤은 스마트폰이나 태블릿에서 손가락으로 3D 장면을 조작할 수 있게 만드는 기능입니다. 마우스 이벤트와 터치 이벤트는 전혀 다른 방식으로 작동하므로 별도로 구현해야 합니다.
OrbitControls를 사용하면 터치 제스처를 자동으로 지원할 수 있습니다.
다음 코드를 살펴봅시다.
// OrbitControls로 터치 제스처 지원
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
const controls = new OrbitControls(camera, renderer.domElement);
// 터치 제스처 설정
controls.enableDamping = true; // 부드러운 감속 효과
controls.dampingFactor = 0.05;
controls.enableZoom = true; // 핀치 줌 허용
controls.enableRotate = true; // 회전 허용
controls.enablePan = true; // 두 손가락 팬 허용
// 회전 속도 조정 (모바일에 맞게)
controls.rotateSpeed = 0.5;
controls.zoomSpeed = 0.8;
// 애니메이션 루프에서 업데이트
function animate() {
requestAnimationFrame(animate);
controls.update(); // damping 사용 시 필수
renderer.render(scene, camera);
}
animate();
이준호 씨는 주말에 부모님 댁에 놀러갔습니다. 저녁 식사 후, 자랑스럽게 포트폴리오 사이트를 보여드렸습니다.
"아빠, 제가 만든 3D 웹사이트예요. 마우스로 드래그하면 모델이 돌아가요!" 아버지는 감탄하며 보셨지만, 어머니는 자기 스마트폰으로 접속하셨습니다.
"어머, 신기하네. 그런데 이걸 어떻게 움직여?" 어머니가 화면을 터치하고 드래그했지만 3D 모델은 꿈쩍도 하지 않았습니다.
이준호 씨는 당황했습니다. "어?
이상한데..." 자기 폰으로도 확인해보니 마찬가지였습니다. 데스크톱에서는 마우스로 완벽하게 조작되는데, 모바일에서는 터치가 전혀 먹히지 않았습니다.
월요일 아침, 또다시 최수진 씨를 찾아갔습니다. "선배님, 터치가 안 돼요." 최수진 씨는 코드를 보더니 웃었습니다.
"마우스 이벤트만 처리했네요. 터치 이벤트는 별개예요." 모바일 터치 컨트롤이란 무엇일까요?
쉽게 비유하자면, 마우스와 터치는 같은 언어를 쓰는 다른 나라 사람과 같습니다. 둘 다 "이동"을 표현하지만 방식이 완전히 다릅니다.
마우스는 click, mousedown, mousemove 이벤트를 사용하고, 터치는 touchstart, touchmove, touchend를 사용합니다. 게다가 터치는 마우스에 없는 기능도 있습니다.
두 손가락으로 줌인/줌아웃하는 핀치 제스처, 두 손가락으로 화면을 이동하는 팬 제스처 등입니다. 이런 것들을 모두 직접 구현하려면 코드가 엄청나게 복잡해집니다.
터치 지원이 없던 시절에는 어땠을까요? 초기 3D 웹사이트들은 데스크톱 전용이었습니다.
모바일 브라우저 성능도 좋지 않았고, 터치 API도 표준화되지 않았으니까요. 하지만 지금은 스마트폰이 데스크톱보다 더 많이 사용됩니다.
포트폴리오 사이트를 만들었는데 면접관이 출퇴근길에 스마트폰으로 보는데 조작이 안 된다면? "이 사람은 모바일 사용자를 전혀 고려하지 않았구나"라는 인상을 주게 됩니다.
바로 이런 문제를 해결하기 위해 OrbitControls 같은 라이브러리가 등장했습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 OrbitControls를 import합니다. 이것은 Three.js 공식 예제 라이브러리에 포함되어 있습니다.
카메라와 렌더러의 DOM 요소를 전달하여 인스턴스를 생성합니다. enableDamping을 true로 설정하면 움직임이 부드럽게 감속됩니다.
마치 아이스링크에서 미끄러지듯이 자연스럽게 멈춥니다. dampingFactor는 감속 강도를 조절합니다.
0.05는 적당한 값입니다. enableZoom, enableRotate, enablePan은 각각 줌, 회전, 이동 기능을 켜고 끄는 스위치입니다.
모바일에서는 핀치 줌이 enableZoom에 해당하고, 한 손가락 드래그가 enableRotate, 두 손가락 드래그가 enablePan입니다. rotateSpeed와 zoomSpeed는 조작 민감도를 조정합니다.
모바일 화면은 작으므로 데스크톱보다 조금 느리게 설정하는 게 좋습니다. 너무 빠르면 손가락을 살짝만 움직여도 모델이 빙글빙글 돌아서 어지럽습니다.
애니메이션 루프에서 controls.update를 매 프레임마다 호출해야 합니다. 특히 damping을 사용할 때는 필수입니다.
이것을 빼먹으면 감속 효과가 작동하지 않습니다. 실무에서는 어떻게 활용할까요? 실제 포트폴리오 사이트에서는 사용자가 3D 모델을 자유롭게 둘러봐야 합니다.
데스크톱 사용자는 마우스로, 모바일 사용자는 터치로 조작하길 기대합니다. OrbitControls는 이 모든 것을 자동으로 처리해줍니다.
별도의 조건문이나 이벤트 리스너 없이 단 몇 줄의 코드로 모든 플랫폼을 지원할 수 있습니다. 많은 3D 웹사이트들이 OrbitControls를 기본으로 사용하고, 필요에 따라 커스터마이징합니다.
예를 들어 Y축 회전만 허용하고 X축은 고정하거나, 줌 범위를 제한하는 식입니다. 하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 controls.update를 빼먹는 것입니다.
damping을 켜놓고 update를 호출하지 않으면 아예 조작이 안 됩니다. 에러 메시지도 없어서 원인을 찾기 어렵습니다.
또 다른 실수는 모바일에서 속도를 조정하지 않는 것입니다. 데스크톱용 속도 그대로 쓰면 모바일에서는 너무 빨라서 정밀한 조작이 어렵습니다.
기기별로 테스트하며 적절한 값을 찾아야 합니다. 스크롤과 충돌하는 문제도 있습니다.
모바일에서 핀치 줌을 하면 브라우저의 기본 줌과 겹칠 수 있습니다. 이럴 때는 CSS로 touch-action: none을 설정하면 됩니다.
다시 이준호 씨의 이야기로 돌아가봅시다. 최수진 씨가 알려준 대로 OrbitControls를 추가한 이준호 씨는 다시 테스트해봤습니다. 이번에는 모바일에서도 완벽했습니다.
한 손가락으로 드래그하면 모델이 돌아가고, 두 손가락으로 핀치하면 줌이 되고, 두 손가락으로 드래그하면 위치가 이동했습니다. 다음 주말, 어머니에게 다시 보여드렸습니다.
"이제 되네! 신기하다!" 어머니가 즐겁게 3D 모델을 이리저리 돌려보시는 모습에 이준호 씨는 뿌듯했습니다.
모바일 터치 컨트롤을 추가하면 훨씬 더 많은 사람들이 여러분의 작품을 즐길 수 있습니다. OrbitControls 덕분에 복잡한 터치 이벤트 처리를 몇 줄로 해결할 수 있습니다.
여러분의 3D 프로젝트에도 꼭 적용해 보세요.
실전 팁
💡 - CSS에서 canvas { touch-action: none; }을 추가하면 브라우저 기본 제스처와 충돌을 방지할 수 있습니다
- controls.minDistance와 maxDistance로 줌 범위를 제한하면 사용자가 너무 가까이/멀리 가는 것을 막을 수 있습니다
- autoRotate 옵션을 켜면 사용자가 조작하지 않을 때 모델이 천천히 회전합니다
5. 테마 변경(Theme Switcher) 구현
이준호 씨의 포트폴리오 사이트가 점점 완성되어 갔습니다. 하지만 디자이너 친구가 한마디 했습니다.
"밤에 보니까 너무 눈부신데? 다크 모드 없어?" 이준호 씨는 생각해보니 요즘 모든 사이트가 다크 모드를 지원한다는 걸 깨달았습니다.
테마 변경 기능은 사용자가 라이트 모드와 다크 모드를 선택할 수 있게 만드는 기능입니다. 마치 방의 조명을 밝게 할지 어둡게 할지 선택하는 것처럼, 웹사이트의 배경색과 조명을 바꿔줍니다.
3D 장면의 경우 배경색뿐 아니라 조명 색상과 강도도 함께 조정해야 자연스럽습니다.
다음 코드를 살펴봅시다.
// 테마 변경 시스템 구현
let currentTheme = 'light';
const scene = new THREE.Scene();
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
scene.add(ambientLight, directionalLight);
function switchTheme(theme) {
currentTheme = theme;
if (theme === 'dark') {
// 다크 모드: 어두운 배경, 부드러운 조명
scene.background = new THREE.Color(0x1a1a1a);
ambientLight.intensity = 0.3;
directionalLight.intensity = 0.5;
document.body.classList.add('dark-mode');
} else {
// 라이트 모드: 밝은 배경, 강한 조명
scene.background = new THREE.Color(0xf0f0f0);
ambientLight.intensity = 0.5;
directionalLight.intensity = 0.8;
document.body.classList.remove('dark-mode');
}
// 로컬 스토리지에 저장
localStorage.setItem('theme', theme);
}
// 페이지 로드 시 저장된 테마 복원
const savedTheme = localStorage.getItem('theme') || 'light';
switchTheme(savedTheme);
어느 날 밤 11시, 이준호 씨는 침대에 누워 스마트폰으로 자기 포트폴리오 사이트를 확인했습니다. 화면을 켜는 순간 눈이 부셨습니다.
새하얀 배경에 밝은 조명이 어두운 방에서는 너무 강렬했습니다. "아, 다크 모드가 있으면 좋겠는데..." 이준호 씨는 요즘 대부분의 앱과 웹사이트가 다크 모드를 지원한다는 걸 떠올렸습니다.
유튜브, 인스타그램, 깃허브... 모두 눈에 편한 어두운 테마를 제공합니다.
다음 날 회사에서 디자이너 친구 김민지 씨에게 물어봤습니다. "3D 사이트에 다크 모드를 추가하려면 어떻게 해야 해?" 민지 씨가 대답했습니다.
"일반 웹페이지는 CSS만 바꾸면 되는데, 3D는 조명도 함께 바꿔야 자연스러워요." 테마 변경 기능이란 무엇일까요? 쉽게 비유하자면, 테마 변경은 마치 카페의 조명을 조절하는 것과 같습니다.
낮에는 밝은 조명으로 활기찬 분위기를 만들고, 밤에는 은은한 조명으로 아늑한 분위기를 만듭니다. 웹사이트도 마찬가지로 낮에는 밝은 테마, 밤에는 어두운 테마를 제공하면 사용자가 훨씬 편안하게 느낍니다.
일반 웹페이지는 배경색과 텍스트 색만 바꾸면 되지만, 3D 장면은 조명 시스템까지 고려해야 합니다. 같은 3D 모델이라도 조명에 따라 완전히 다르게 보이니까요.
테마 변경이 없던 시절에는 어땠을까요? 예전에는 대부분의 웹사이트가 흰색 배경에 검은색 텍스트를 사용했습니다.
"화면은 원래 밝은 거야"라고 생각했죠. 하지만 사람들이 밤늦게까지 스마트폰을 보면서 눈의 피로를 호소하기 시작했습니다.
연구 결과, 어두운 환경에서 밝은 화면을 오래 보면 눈의 피로가 급격히 증가한다는 것이 밝혀졌습니다. 또한 OLED 화면은 검은색 픽셀이 꺼지므로 다크 모드에서 배터리도 절약됩니다.
바로 이런 이유로 다크 모드가 현대 웹의 표준이 되었습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 currentTheme 변수로 현재 테마를 추적합니다. scene에 두 가지 조명을 추가합니다.
AmbientLight는 전체적으로 은은하게 비추는 조명이고, DirectionalLight는 특정 방향에서 강하게 비추는 조명입니다. switchTheme 함수는 테마를 전환하는 핵심 로직입니다.
테마가 dark이면 배경을 어두운 회색(0x1a1a1a)으로 바꾸고, 조명 강도를 낮춥니다. 라이트 모드에서는 밝은 회색(0xf0f0f0) 배경에 강한 조명을 사용합니다.
document.body.classList로 HTML body에 dark-mode 클래스를 추가하거나 제거합니다. 이렇게 하면 CSS로 UI 요소들의 색상도 함께 바꿀 수 있습니다.
localStorage에 사용자의 테마 선택을 저장합니다. 다음에 사이트를 방문했을 때도 같은 테마가 적용되도록 하는 겁니다.
마치 카페에서 단골손님의 선호도를 기억하는 것처럼요. 페이지 로드 시 저장된 테마를 읽어와서 복원합니다.
사용자가 어제 다크 모드를 선택했다면 오늘도 다크 모드로 시작합니다. 실무에서는 어떻게 활용할까요? 실제 포트폴리오 사이트에서는 우측 상단에 태양/달 아이콘 토글 버튼을 만듭니다.
사용자가 클릭하면 switchTheme 함수가 호출되어 즉시 테마가 바뀝니다. 많은 고급 사이트들은 사용자의 시스템 설정을 자동으로 감지합니다.
window.matchMedia를 사용하면 운영체제의 다크 모드 설정을 읽을 수 있습니다. 사용자가 시스템 전체를 다크 모드로 설정했다면 웹사이트도 자동으로 다크 모드로 시작하는 거죠.
3D 모델의 재질에 따라 테마별로 조명을 더 섬세하게 조정하기도 합니다. 금속 재질은 다크 모드에서 반사가 강하므로 조명을 약하게 하고, 무광 재질은 조명을 조금 더 강하게 하는 식입니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 배경색만 바꾸고 조명은 그대로 두는 것입니다. 검은 배경에 밝은 조명을 그대로 쓰면 3D 모델이 과하게 밝아 보여서 부자연스럽습니다.
배경과 조명은 항상 함께 조정해야 합니다. 또 다른 실수는 테마 전환 시 애니메이션이 없는 것입니다.
갑자기 확 바뀌면 눈이 부실 수 있습니다. CSS transition을 추가하거나 조명을 서서히 페이드하면 훨씬 부드럽습니다.
색맹이나 저시력 사용자를 위해 충분한 대비를 유지하는 것도 중요합니다. 너무 어두운 회색에 약간 밝은 회색 텍스트를 쓰면 가독성이 떨어집니다.
WCAG 가이드라인을 참고하세요. 다시 이준호 씨의 이야기로 돌아가봅시다. 테마 변경 기능을 추가한 이준호 씨는 그날 밤 다시 침대에서 사이트를 열어봤습니다.
이번에는 다크 모드 버튼을 탭했습니다. 부드럽게 배경이 어두워지고 조명이 은은해졌습니다.
"완벽해!" 눈이 전혀 피로하지 않았습니다. 다음 날 친구들에게 다시 공유했습니다.
"와, 다크 모드 있네! 완전 프로같아!" 친구들의 반응에 이준호 씨는 뿌듯함을 느꼈습니다.
테마 변경 기능은 단순해 보이지만 사용자 경험에 큰 영향을 미칩니다. 특히 밤에 자주 웹을 사용하는 사람들에게는 필수 기능입니다.
여러분의 3D 프로젝트에도 꼭 추가해 보세요.
실전 팁
💡 - prefers-color-scheme 미디어 쿼리로 사용자 시스템 설정을 자동 감지할 수 있습니다
- CSS에서 transition: background-color 0.3s ease를 추가하면 테마 전환이 부드럽습니다
- 조명 강도를 바꿀 때도 TweenJS나 GSAP로 애니메이션을 주면 더 자연스럽습니다
6. UI/UX 통합 및 인터랙션
이준호 씨의 포트폴리오 사이트에 3D 기능은 완벽했습니다. 하지만 UX 디자이너 박지은 씨가 피드백을 줬습니다.
"3D는 멋진데, 사용자가 뭘 할 수 있는지 모르겠어요. 버튼도 없고, 안내도 없고..." 이준호 씨는 깨달았습니다.
기술만 좋다고 끝이 아니라는 것을요.
UI/UX 통합은 3D 장면과 일반 웹 UI 요소들을 조화롭게 결합하는 작업입니다. 마치 영화에서 배우와 CG가 자연스럽게 어우러지듯이, HTML 버튼, 텍스트, 메뉴가 3D 공간과 잘 어울려야 합니다.
사용자에게 명확한 피드백과 안내를 제공하는 것이 핵심입니다.
다음 코드를 살펴봅시다.
// 3D 장면 위에 HTML UI 오버레이
<div class="ui-overlay">
<div class="controls-hint">
<p>마우스 드래그: 회전</p>
<p>스크롤: 줌</p>
</div>
<button id="reset-camera" class="ui-button">
카메라 초기화
</button>
</div>
// JavaScript에서 UI와 3D 연결
const resetButton = document.getElementById('reset-camera');
resetButton.addEventListener('click', () => {
// 카메라를 원래 위치로 부드럽게 이동
gsap.to(camera.position, {
x: 0, y: 5, z: 10,
duration: 1.5,
ease: "power2.inOut"
});
controls.target.set(0, 0, 0);
});
// 3D 객체 클릭 감지 (Raycaster)
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
canvas.addEventListener('click', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const clickedObject = intersects[0].object;
showInfoPanel(clickedObject.name); // 정보 패널 표시
}
});
이준호 씨는 포트폴리오 사이트를 완성하고 여러 커뮤니티에 공유했습니다. 반응은 엇갈렸습니다.
개발자들은 "3D 구현이 멋지네요!"라고 칭찬했지만, 일반 사용자들은 "이거 어떻게 쓰는 거예요?"라고 물었습니다. 회사 UX 디자이너 박지은 씨에게 의견을 구했습니다.
지은 씨는 사이트를 2분 정도 둘러보더니 말했습니다. "기술은 훌륭한데, 사용자 관점에서는 어렵네요.
뭘 클릭할 수 있는지, 어떻게 조작하는지 알려주는 게 없어요." 이준호 씨는 충격을 받았습니다. 본인은 모든 기능을 알고 있으니 당연하게 느껴졌지만, 처음 방문한 사람은 전혀 감을 잡을 수 없었던 겁니다.
UI/UX 통합이란 무엇일까요? 쉽게 비유하자면, UI/UX 통합은 마치 박물관의 안내판과 같습니다.
멋진 전시품(3D 모델)만 있으면 뭐하나요? 관람객이 그게 무엇인지, 어떻게 보는지 모르면 의미가 없습니다.
안내판, 화살표, 설명 패널이 있어야 관람객이 전시를 제대로 즐길 수 있습니다. 3D 웹사이트도 마찬가지입니다.
버튼, 툴팁, 안내 메시지 같은 UI 요소들이 있어야 사용자가 기능을 발견하고 활용할 수 있습니다. UI가 부실하던 시절에는 어땠을까요?
초창기 3D 웹사이트들은 기술 시연에만 집중했습니다. "WebGL로 이런 것도 가능해요!"라고 보여주는 데모였죠.
하지만 실제 사용자들은 5초 보다가 "뭐 하는 사이트지?"하고 떠났습니다. 사용성 연구 결과, 사용자는 새로운 사이트에 도착하면 평균 3초 안에 무엇을 할 수 있는지 파악하려 한다고 합니다.
명확한 안내가 없으면 혼란스러워하고 이탈합니다. 바로 이런 문제를 해결하기 위해 3D와 UI의 조화로운 통합이 중요해졌습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 HTML에서 ui-overlay라는 div를 만듭니다.
이것은 3D 캔버스 위에 겹쳐지는 레이어입니다. CSS로 position: absolute를 주면 3D 장면 위에 떠 있게 됩니다.
controls-hint는 조작법을 알려주는 툴팁입니다. 처음 방문한 사용자는 "마우스 드래그로 회전할 수 있구나"라고 바로 이해합니다.
몇 초 후 자동으로 사라지게 만들면 더 좋습니다. reset-camera 버튼은 사용자가 카메라를 이상한 각도로 돌려놨을 때 원래대로 되돌리는 기능입니다.
JavaScript에서 클릭 이벤트를 연결하고, GSAP 라이브러리로 카메라 위치를 부드럽게 애니메이션합니다. Raycaster는 3D 객체 클릭을 감지하는 도구입니다.
마우스 클릭 위치를 3D 공간의 좌표로 변환하고, 어떤 객체가 그 위치에 있는지 찾아냅니다. 클릭된 객체의 이름을 가져와서 정보 패널을 띄울 수 있습니다.
마우스 좌표를 -1에서 1 사이의 정규화된 값으로 변환하는 부분이 중요합니다. Three.js의 좌표계는 화면 중심이 (0, 0)이고, 왼쪽 위가 (-1, 1), 오른쪽 아래가 (1, -1)입니다.
실무에서는 어떻게 활용할까요? 실제 포트폴리오 사이트에서는 다양한 UI 요소를 배치합니다. 상단에 네비게이션 메뉴, 하단에 프로젝트 설명, 측면에 소셜 미디어 링크 등입니다.
3D 모델의 특정 부분을 클릭하면 상세 정보가 팝업으로 뜨도록 만듭니다. 예를 들어 캐릭터의 옷을 클릭하면 "이 프로젝트는 Three.js와 Blender를 사용했습니다"라는 설명이 나타나는 식입니다.
유명한 3D 포트폴리오 사이트들은 스크롤에 따라 3D 장면이 변하도록 만듭니다. 아래로 스크롤하면 카메라가 이동하면서 다른 작품들을 차례로 보여주는 스크롤 애니메이션이죠.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 UI를 너무 많이 넣는 것입니다. 화면에 버튼이 수십 개 있으면 오히려 산만해집니다.
핵심 기능만 남기고 나머지는 숨겨두세요. 또 다른 실수는 모바일에서 버튼이 너무 작은 것입니다.
데스크톱에서는 괜찮아 보여도 스마트폰에서는 손가락으로 누르기 어려울 수 있습니다. 최소 44x44픽셀 이상의 터치 영역을 확보하세요.
접근성도 중요합니다. 키보드만으로도 모든 기능을 사용할 수 있어야 하고, 스크린 리더 사용자를 위해 적절한 ARIA 속성을 추가해야 합니다.
다시 이준호 씨의 이야기로 돌아가봅시다. 박지은 씨의 조언을 받아들인 이준호 씨는 UI를 대폭 개선했습니다. 조작법 안내를 추가하고, 클릭 가능한 요소에 호버 효과를 넣고, 카메라 리셋 버튼도 만들었습니다.
다시 커뮤니티에 공유했더니 반응이 완전히 달라졌습니다. "사용하기 편하네요!" "디자인이 세련됐어요!" 이전에는 기술만 칭찬받았는데, 이제는 사용자 경험까지 인정받게 되었습니다.
UI/UX 통합은 기술적 완성도만큼이나 중요합니다. 아무리 멋진 3D를 만들어도 사용자가 이해하지 못하면 소용없습니다.
명확한 안내와 직관적인 인터랙션으로 사용자를 배려하세요.
실전 팁
💡 - 처음 방문한 사용자를 위해 3-5초간 표시되는 조작 힌트를 추가하세요
- 클릭 가능한 3D 객체는 호버 시 색상이나 크기가 살짝 변하도록 만드세요
- GSAP 같은 애니메이션 라이브러리를 사용하면 UI 전환이 훨씬 부드럽습니다
7. 접근성(Accessibility) 고려
이준호 씨의 포트폴리오 사이트가 거의 완성되었습니다. 하지만 회사 접근성 전문가 최민수 씨가 테스트해보더니 말했습니다.
"3D는 멋진데, 스크린 리더로는 아무것도 안 들려요. 키보드로도 조작이 안 되고요." 이준호 씨는 접근성에 대해 전혀 생각하지 못했다는 걸 깨달았습니다.
접근성은 장애가 있는 사용자도 웹사이트를 동등하게 이용할 수 있도록 만드는 것입니다. 마치 건물에 경사로와 엘리베이터를 설치하는 것처럼, 웹사이트에도 스크린 리더 지원, 키보드 내비게이션, 충분한 색상 대비 같은 기능을 추가해야 합니다.
3D 콘텐츠는 시각적이지만 대체 텍스트와 설명을 제공하면 모두가 즐길 수 있습니다.
다음 코드를 살펴봅시다.
// 접근성을 위한 HTML 구조
<main role="main">
<h1>3D 포트폴리오 - 이준호</h1>
<!-- 스크린 리더를 위한 설명 -->
<div class="sr-only" aria-live="polite" id="scene-description">
Three.js로 제작된 3D 캐릭터 모델입니다.
마우스나 키보드로 회전하고 확대할 수 있습니다.
</div>
<!-- 키보드로 조작 가능한 버튼들 -->
<div class="controls" role="toolbar" aria-label="3D 장면 컨트롤">
<button
id="rotate-left"
aria-label="왼쪽으로 회전"
tabindex="0">
← 회전
</button>
<button
id="rotate-right"
aria-label="오른쪽으로 회전"
tabindex="0">
회전 →
</button>
</div>
<canvas id="webgl-canvas" aria-label="3D 포트폴리오 장면"></canvas>
</main>
// JavaScript로 키보드 내비게이션 추가
document.addEventListener('keydown', (event) => {
switch(event.key) {
case 'ArrowLeft':
model.rotation.y -= 0.1;
updateDescription('모델을 왼쪽으로 회전했습니다');
break;
case 'ArrowRight':
model.rotation.y += 0.1;
updateDescription('모델을 오른쪽으로 회전했습니다');
break;
case '+':
camera.position.z -= 1;
updateDescription('확대했습니다');
break;
case '-':
camera.position.z += 1;
updateDescription('축소했습니다');
break;
}
});
function updateDescription(text) {
const desc = document.getElementById('scene-description');
desc.textContent = text;
}
이준호 씨는 포트폴리오 사이트를 완성하고 회사 동료들에게 자랑했습니다. 대부분 긍정적인 반응이었지만, 접근성 전문가 최민수 씨는 조용히 노트북을 꺼냈습니다.
민수 씨는 스크린 리더 프로그램을 켜고 이준호 씨의 사이트를 탐색해봤습니다. 몇 분 후 고개를 저으며 말했습니다.
"3D 장면에 대한 설명이 전혀 없네요. 시각장애인 사용자는 여기에 뭐가 있는지 알 수 없어요." 그다음 마우스를 치우고 키보드만으로 조작해봤습니다.
Tab 키를 아무리 눌러도 3D 장면과 상호작용할 방법이 없었습니다. "마우스가 없으면 아무것도 못 하네요." 이준호 씨는 충격을 받았습니다.
"접근성... 생각도 못 했어요." 민수 씨가 웃으며 말했습니다.
"많은 개발자가 그래요. 하지만 전 세계 인구의 15%가 어떤 형태로든 장애를 가지고 있어요.
그들도 여러분의 작품을 즐길 권리가 있죠." 접근성이란 무엇일까요? 쉽게 비유하자면, 접근성은 마치 유니버설 디자인과 같습니다.
계단만 있는 건물은 휠체어 사용자가 들어갈 수 없습니다. 하지만 경사로를 함께 만들면 누구나 들어올 수 있습니다.
오히려 유모차를 끄는 부모나 무거운 짐을 나르는 사람도 편해집니다. 웹 접근성도 마찬가지입니다.
시각장애인을 위해 만든 스크린 리더 지원은 검색엔진 최적화(SEO)에도 도움이 됩니다. 키보드 내비게이션은 파워유저들이 더 빠르게 사이트를 탐색할 수 있게 해줍니다.
접근성을 고려하지 않던 시절에는 어땠을까요? 예전에는 "장애인 사용자는 소수니까 신경 쓸 필요 없어"라고 생각하는 사람들이 많았습니다.
하지만 법적으로도 접근성이 의무화되고 있습니다. 미국의 ADA, 유럽의 EAA, 한국의 웹 접근성 인증제도 등이 있습니다.
대기업들이 접근성 미비로 소송을 당하는 사례도 늘고 있습니다. 도덕적 책임뿐 아니라 법적 리스크도 있는 거죠.
더 중요한 건, 접근성 좋은 웹사이트는 모든 사용자에게 더 나은 경험을 제공한다는 점입니다. 바로 이런 이유로 웹 접근성이 현대 웹 개발의 필수 요소가 되었습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 시맨틱 HTML을 사용합니다.
main, h1 같은 태그는 스크린 리더에게 페이지 구조를 알려줍니다. "여기가 메인 콘텐츠고, 여기가 제목이에요"라고 말해주는 거죠.
sr-only 클래스는 "screen reader only"의 약자입니다. 화면에는 보이지 않지만 스크린 리더는 읽습니다.
시각장애인 사용자에게 3D 장면에 대한 설명을 제공합니다. **aria-live="polite"**는 이 영역의 내용이 변경되면 스크린 리더가 자동으로 읽어준다는 뜻입니다.
사용자가 모델을 회전시키면 "모델을 왼쪽으로 회전했습니다"라고 음성으로 알려줍니다. **role="toolbar"**와 aria-label은 UI 요소의 역할과 이름을 명시합니다.
스크린 리더 사용자가 Tab 키로 이동할 때 "3D 장면 컨트롤 툴바, 왼쪽으로 회전 버튼"처럼 상세하게 읽어줍니다. **tabindex="0"**은 키보드 포커스를 받을 수 있게 만듭니다.
Tab 키로 버튼들을 순회하며 선택할 수 있습니다. JavaScript에서 keydown 이벤트를 추가하여 화살표 키로 모델을 조작할 수 있게 합니다.
마우스가 없어도, 터치가 안 돼도 키보드만으로 모든 기능을 사용할 수 있습니다. updateDescription 함수는 사용자 액션에 따라 스크린 리더 설명을 업데이트합니다.
시각적으로는 모델이 회전하는 게 보이지만, 시각장애인 사용자는 음성으로 피드백을 받습니다. 실무에서는 어떻게 활용할까요? 실제 포트폴리오 사이트에서는 각 3D 모델에 대한 상세 설명을 텍스트로 제공합니다.
"이 모델은 Blender로 제작한 사이버펑크 스타일의 로봇입니다. 금속 재질과 네온 조명이 특징입니다"처럼요.
키보드 단축키를 문서화하여 사용자에게 알려줍니다. "화살표 키: 회전, +/-: 줌, R: 초기화"처럼 명확하게 안내합니다.
색맹 사용자를 위해 색상에만 의존하지 않습니다. 예를 들어 "빨간 버튼을 클릭하세요"가 아니라 "다음 버튼을 클릭하세요"처럼 명확한 레이블을 사용합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 ARIA 속성을 남용하는 것입니다. 네이티브 HTML로 해결할 수 있으면 그게 최선입니다.
button 태그는 이미 역할이 명확하므로 굳이 role="button"을 추가할 필요 없습니다. 또 다른 실수는 키보드 포커스를 시각적으로 표시하지 않는 것입니다.
CSS로 outline을 없애면 키보드 사용자는 지금 어디에 있는지 알 수 없습니다. 포커스 스타일을 커스터마이징하되 반드시 보이게 만드세요.
자동화된 접근성 테스트 도구(axe, Lighthouse)를 활용하되, 실제 스크린 리더로도 테스트해봐야 합니다. 자동 도구는 일부 문제만 잡아냅니다.
다시 이준호 씨의 이야기로 돌아가봅시다. 최민수 씨의 조언을 받아들인 이준호 씨는 접근성 기능을 추가했습니다. ARIA 속성을 넣고, 키보드 내비게이션을 구현하고, 상세한 설명을 작성했습니다.
일주일 후 민수 씨가 다시 테스트해봤습니다. 이번에는 스크린 리더가 장면을 자세히 설명했고, 키보드만으로 모든 기능을 사용할 수 있었습니다.
"완벽해요! 이제 진짜 프로페셔널한 사이트네요." 더 놀라운 일이 있었습니다.
한 채용 담당자가 이메일을 보내왔습니다. "포트폴리오에 접근성까지 고려한 걸 보고 감동했습니다.
면접 제안을 드리고 싶습니다." 접근성 배려가 오히려 경쟁력이 된 겁니다. 접근성은 선택이 아니라 필수입니다.
모든 사람이 여러분의 작품을 즐길 수 있어야 합니다. 약간의 추가 노력으로 훨씬 더 많은 사람들에게 다가갈 수 있습니다.
여러분의 3D 프로젝트에도 꼭 접근성을 추가해 보세요.
실전 팁
💡 - WebAIM의 WAVE 도구나 Chrome의 Lighthouse로 자동 접근성 검사를 하세요
- 실제 스크린 리더(NVDA, JAWS, VoiceOver)로 직접 테스트해보는 게 가장 확실합니다
- skip to main content 링크를 추가하면 키보드 사용자가 네비게이션을 건너뛸 수 있습니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
GitHub와 Vercel로 시작하는 배포 완벽 가이드
코드를 작성했다면 이제 세상에 공개할 차례입니다. GitHub에 코드를 올리고 Vercel로 배포하는 전체 과정을 실무 상황 스토리로 풀어냅니다. 초급 개발자도 따라하면서 자연스럽게 배포 프로세스를 이해할 수 있습니다.
Three.js 고급 코드 분석 완벽 가이드
Three.js 공식 예제를 분석하고 복잡한 씬 구조를 이해하는 방법부터 커스텀 Shader, PostProcessing, 성능 최적화, 디버깅까지 실무에서 바로 활용할 수 있는 고급 기법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 상황 스토리와 비유로 풀어낸 가이드입니다.
Three.js 충돌 감지 완벽 가이드
3D 웹 게임에서 캐릭터가 벽을 뚫고 지나가는 문제를 해결하는 충돌 감지 기술을 배웁니다. Bounding Box부터 물리 엔진까지 실전 예제로 완벽하게 마스터해보세요.
Three.js Raycaster 상호작용 구현 완벽 가이드
Three.js의 Raycaster를 활용하여 3D 객체와의 상호작용을 구현하는 방법을 초급자 눈높이에서 설명합니다. 마우스 클릭, 터치 이벤트, 하이라이트 효과까지 실전 예제와 함께 배워봅니다.
GSAP 애니메이션 라이브러리 완벽 가이드
웹 애니메이션의 표준, GSAP를 처음부터 제대로 배워봅니다. 기본 사용법부터 Timeline, Easing, Three.js 통합까지 실무에서 바로 쓸 수 있는 핵심 내용을 다룹니다.