인터랙티브 3D 명함 만들기 - 사이드 프로젝트 기록
네트워킹 행사에서 종이 명함을 주고받을 때마다 생각했습니다. "개발자라면서 왜 종이 명함이지?" 그래서 주말을 투자해서 3D 명함을 만들었습니다.
컨셉 #
기존 명함의 문제점:
- 받자마자 잊어버림
- 차별화가 어려움
- 연락처 수동 입력 필요
3D 명함의 목표:
- 기억에 남는 첫인상
- 개발자 정체성 표현
- 원클릭으로 연락처 저장
기획 #
명함을 QR로 스캔하면 3D 씬이 열리고, 제 정보가 인터랙티브하게 표시됩니다.
구성 요소:
- 3D 카드 모델 (실제 명함처럼)
- 회전/줌 인터랙션
- 클릭하면 상세 정보 표시
- 연락처 저장 버튼
개발 #
명함 모델 #
실제 명함 비율(90mm × 50mm)로 만들었습니다.
const cardGeometry = new THREE.BoxGeometry(9, 5, 0.05);
// 앞면 텍스처
const frontCanvas = document.createElement('canvas');
const frontCtx = frontCanvas.getContext('2d');
frontCanvas.width = 900;
frontCanvas.height = 500;
// 배경
frontCtx.fillStyle = '#1a1a2e';
frontCtx.fillRect(0, 0, 900, 500);
// 이름
frontCtx.font = 'bold 48px Pretendard';
frontCtx.fillStyle = '#ffffff';
frontCtx.fillText('홍길동', 50, 200);
// 직함
frontCtx.font = '24px Pretendard';
frontCtx.fillStyle = '#888888';
frontCtx.fillText('Frontend Developer', 50, 250);
const frontTexture = new THREE.CanvasTexture(frontCanvas);
인터랙션 #
마우스/터치로 카드를 회전시킬 수 있습니다.
let isDragging = false;
let previousPosition = { x: 0, y: 0 };
canvas.addEventListener('pointerdown', (e) => {
isDragging = true;
previousPosition = { x: e.clientX, y: e.clientY };
});
canvas.addEventListener('pointermove', (e) => {
if (!isDragging) return;
const deltaX = e.clientX - previousPosition.x;
const deltaY = e.clientY - previousPosition.y;
card.rotation.y += deltaX * 0.01;
card.rotation.x += deltaY * 0.01;
previousPosition = { x: e.clientX, y: e.clientY };
});
canvas.addEventListener('pointerup', () => {
isDragging = false;
});
뒤집기 애니메이션 #
더블클릭하면 카드가 뒤집힙니다.
let isFlipped = false;
canvas.addEventListener('dblclick', () => {
gsap.to(card.rotation, {
y: isFlipped ? 0 : Math.PI,
duration: 0.6,
ease: 'power2.inOut'
});
isFlipped = !isFlipped;
});
뒷면 정보 #
뒷면에는 QR 코드와 연락처를 넣었습니다.
const backCanvas = document.createElement('canvas');
const backCtx = backCanvas.getContext('2d');
// QR 코드 생성 (qrcode 라이브러리 사용)
const qr = await QRCode.toCanvas(contactVCard, { width: 200 });
backCtx.drawImage(qr, 350, 150);
// 소셜 아이콘들
backCtx.fillText('github.com/username', 50, 400);
backCtx.fillText('linkedin.com/in/username', 50, 440);
연락처 저장 (vCard) #
연락처 저장 버튼을 누르면 vCard 파일이 다운로드됩니다.
function downloadVCard() {
const vcard = `BEGIN:VCARD
VERSION:3.0
FN:홍길동
ORG:회사명
TITLE:Frontend Developer
TEL:010-1234-5678
EMAIL:[email protected]
URL:https://portfolio.com
END:VCARD`;
const blob = new Blob([vcard], { type: 'text/vcard' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'contact.vcf';
link.click();
URL.revokeObjectURL(url);
}
시각 효과 #
홀로그램 효과 #
명함에 홀로그램 느낌을 주고 싶었습니다.
const hologramMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uTexture: { value: frontTexture }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform sampler2D uTexture;
varying vec2 vUv;
void main() {
vec4 tex = texture2D(uTexture, vUv);
// 무지개빛 효과
float rainbow = sin(vUv.x * 10.0 + uTime) * 0.5 + 0.5;
vec3 hologram = vec3(
sin(rainbow * 6.28) * 0.5 + 0.5,
sin(rainbow * 6.28 + 2.09) * 0.5 + 0.5,
sin(rainbow * 6.28 + 4.18) * 0.5 + 0.5
);
// 텍스처와 혼합
vec3 color = mix(tex.rgb, hologram, 0.1);
gl_FragColor = vec4(color, 1.0);
}
`
});
파티클 배경 #
배경에 파티클을 띄워서 분위기를 냈습니다.
const particleCount = 500;
const positions = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
positions[i * 3] = (Math.random() - 0.5) * 20;
positions[i * 3 + 1] = (Math.random() - 0.5) * 20;
positions[i * 3 + 2] = (Math.random() - 0.5) * 20;
}
const particleGeometry = new THREE.BufferGeometry();
particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const particleMaterial = new THREE.PointsMaterial({
size: 0.05,
color: 0x6c5ce7,
transparent: true,
opacity: 0.6
});
const particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);
모바일 최적화 #
명함은 주로 폰으로 볼 거라서 모바일 최적화가 중요했습니다.
터치 제스처 #
핀치 줌, 두 손가락 회전을 구현했습니다.
let initialDistance = 0;
let initialRotation = 0;
canvas.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
initialDistance = getDistance(e.touches[0], e.touches[1]);
initialRotation = getAngle(e.touches[0], e.touches[1]);
}
});
canvas.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
// 줌
const distance = getDistance(e.touches[0], e.touches[1]);
const scale = distance / initialDistance;
camera.position.z = 10 / scale;
// 회전
const rotation = getAngle(e.touches[0], e.touches[1]);
card.rotation.z = rotation - initialRotation;
}
});
성능 #
파티클 수를 줄이고, 셰이더를 단순화했습니다.
const isMobile = /iPhone|iPad|Android/i.test(navigator.userAgent);
const particleCount = isMobile ? 100 : 500;
배포 #
정적 호스팅 #
Vercel에 배포했습니다. 빌드 결과물이 2MB 이하라 무료 플랜으로 충분했습니다.
QR 코드 #
실제 종이 명함에 QR 코드를 인쇄했습니다. 스캔하면 3D 명함 페이지로 연결됩니다.
종이 명함 앞면:
- 이름, 직함
- QR 코드 (3D 명함 링크)
3D 명함:
- 인터랙티브 체험
- 연락처 저장
- 포트폴리오 링크
반응 #
네트워킹 행사에서 실제로 사용해봤습니다.
긍정적 반응:
- "오, 이거 직접 만드셨어요?"
- "진짜 개발자스럽네요"
- "저도 만들어주세요" (여러 번 들음)
의외의 문제:
- 네트워크가 느린 곳에서 로딩이 오래 걸림
- 어르신들은 QR 스캔에 익숙하지 않음
- 배터리가 빨리 닳는다는 피드백
개선 사항 #
실제 사용 후 개선한 것들:
- 프리로더 추가: 로딩 중에 스켈레톤 UI 표시
- 오프라인 지원: Service Worker로 캐싱
- 저전력 모드: 애니메이션 최소화 옵션
- 접근성: 3D 외에 일반 텍스트 버전 제공
// 저전력 모드 감지
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (prefersReducedMotion) {
// 정적 이미지로 폴백
showStaticVersion();
}
소스 코드 #
GitHub에 공개했습니다. Fork해서 자신만의 명함을 만들어보세요.
필요한 것:
- 프로필 이미지
- 연락처 정보
- (선택) 커스텀 배경/효과
주말 프로젝트로 시작했는데, 생각보다 반응이 좋아서 뿌듯합니다. 개발자답게, 코드로 자신을 소개하는 것도 좋은 방법인 것 같습니다.