๐ŸŒฉ๏ธ ๊ท€์‹ ์ด ๋‚˜์˜ค๋Š” ๋‚ ์”จ ์•ฑ ๋งŒ๋“ค๊ธฐ: 3D ์›น ๊ฐœ๋ฐœ์˜ ์œผ์Šค์Šคํ•œ ์—ฌ์ •

๋ฐœํ–‰: (2025๋…„ 12์›” 4์ผ ์˜คํ›„ 12:17 GMT+9)
6 min read
์›๋ฌธ: Dev.to

Source: Dev.to

์†Œ๊ฐœ

์•ˆ๊ฐœ๊ฐ€ ๋ผ๊ณ  ๋ฒˆ๊ฐœ๊ฐ€ ์น˜๋ฉด, ์ด์ œ ๋” ์ด์ƒ ์บ”์ž์Šค์— ์žˆ์ง€ ์•Š๋‹ค๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €๋Š” ๋Œ€๊ธฐ ์‹œ๊ฐ ํšจ๊ณผ์™€ ์‹ค์šฉ์ ์ธ ๊ธฐ๋Šฅ์„ ๊ฒฐํ•ฉํ•œ ๋ฌด์–ธ๊ฐ€๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹คโ€”๋ฌด์„ญ๊ฒŒ ๋А๊ปด์ง€๋Š” ๋‚ ์”จ ์•ฑ ๋ง์ด์ฃ . ๊ทธ ๊ฒฐ๊ณผ๊ฐ€ Eerie Weather App์ด๋ฉฐ, ์ „ ์„ธ๊ณ„ ๋„์‹œ๋“ค์˜ ์‹ค์‹œ๊ฐ„ ์ƒํ™ฉ์— ๋ฐ˜์‘ํ•˜๋Š” ์œ ๋ น ๊ฐ™์€ ๋– ๋‹ค๋‹ˆ๋Š” ์ง‘์„ 3D ๋‚ ์”จ ์‹œ๊ฐํ™”๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋น„๊ฐ€ ์˜ค๋ฉด ํ•˜๋Š˜์ด ์–ด๋‘์›Œ์ง€๊ณ  ๋ฌผ๋ฐฉ์šธ์ด ์ถ”๊ฐ€๋˜๋ฉฐ, ๋‡Œ์šฐ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋พฐ์กฑํ•œ ๋ฒˆ๊ฐœ๊ฐ€ ๋‚ด๋ฆฌ์น˜๊ณ , ๊ฐ ๋‚ ์”จ ์œ ํ˜•๋งˆ๋‹ค ๊ณ ์œ ์˜ ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ๊ณผ ์ฃผ๋ณ€ ์†Œ๋ฆฌ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๋””์ž์ธ

์ƒ‰ ๊ตฌ์„ฑํ‘œ

  • ๋ฐฐ๊ฒฝ: #1a1a2e (๊นŠ์€ ํŒŒ๋ž€โ€‘๋ณด๋ผ) โ€“ ํญํ’ ์ „ ํ•˜๋Š˜์ฒ˜๋Ÿผ.
  • ๊ฐ•์กฐ: #460809 โ†’ #f4320b (ํ˜ธ๋ฒ„ ์‹œ) โ€“ ํ”ผ์ฒ˜๋Ÿผ ๋ถ‰์€ ํ•˜์ด๋ผ์ดํŠธ.
  • ํ…์ŠคํŠธ ๊ธ€๋กœ์šฐ: ๋งฅ๋™ํ•˜๋Š” ํ…์ŠคํŠธ ๊ทธ๋ฆผ์ž๋ฅผ ๊ฐ€์ง„ ์—ํ…Œ๋ฆฌ์–ผ ๋ ˆ๋“œ.
h3 {
  color: #ff6b6b;
  text-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
}

ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ

โ€œOctober Crowโ€ ํฐํŠธ๋Š” ํ†ฑ๋‹ˆ ๋ชจ์–‘์˜ ์†์œผ๋กœ ๊ทธ๋ฆฐ ๋А๋‚Œ์„ ์ค๋‹ˆ๋‹ค.

@font-face {
  font-family: "October Crow";
  src: url("/fonts/October Crow.ttf") format("truetype");
}
body {
  font-family: "October Crow", Arial, sans-serif;
}

์ปค์Šคํ…€ ๋ฒ„ํŠผ

๋ฒ„ํŠผ์€ ์˜ค๋ž˜๋œ ์„ํŒ์ฒ˜๋Ÿผ ๋ณด์ด๋Š” ์ธ๋ผ์ธ SVG ๋ฐฐ๊ฒฝ์œผ๋กœ ์Šคํƒ€์ผ๋ง๋ฉ๋‹ˆ๋‹ค.

/* ์˜ˆ์‹œ ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ */
background-image: url('data:image/svg+xml,');

Three.js ์„ค์ •

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.fog = new THREE.Fog(0x1a1a2e, 10, 50);

์•ˆ๊ฐœ๋Š” ๊นŠ์ด๋ฅผ ๋”ํ•˜๊ณ  โ€œ๋ฌด์–ธ๊ฐ€ ์ˆจ์–ด ์žˆ๋Š”โ€ ๋ถ„์œ„๊ธฐ๋ฅผ ์กฐ์„ฑํ•ฉ๋‹ˆ๋‹ค.

์œ ๋ น์˜ ์ง‘ ๋ชจ๋ธ

loader.load("models/forest_house.glb", (gltf) => {
  house = gltf.scene;
  house.userData.centerY = -center.y - 2.5;
  scene.add(house);
});

// Animation loop
house.position.y = house.userData.centerY + Math.sin(time * 0.5) * 0.2;

์ง‘์ด ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์œ„์•„๋ž˜๋กœ ํ”๋“ค๋ฆฌ๋ฉฐ ๋‚ ์”จ์— ๋ฐ˜์‘ํ•ฉ๋‹ˆ๋‹ค.

์กฐ๋ช…

const ambientLight = new THREE.AmbientLight(0x9999cc, 0.4);      // Moonlit base
const directionalLight = new THREE.DirectionalLight(0xaaaadd, 0.3); // Soft moonlight
const rimLight = new THREE.DirectionalLight(0x6666aa, 0.2);          // Subtle backlight

๋ฒˆ๊ฐœ ํšจ๊ณผ

function createLightningBolt(startX, startY, startZ, endX, endY, endZ) {
  const points = [];
  const segments = 8 + Math.floor(Math.random() * 6);

  for (let i = 0; i < segments; i++) {
    // generate jittered points between start and end
    const t = i / (segments - 1);
    const x = THREE.MathUtils.lerp(startX, endX, t) + (Math.random() - 0.5) * 0.5;
    const y = THREE.MathUtils.lerp(startY, endY, t) + (Math.random() - 0.5) * 0.5;
    const z = THREE.MathUtils.lerp(startZ, endZ, t) + (Math.random() - 0.5) * 0.5;
    points.push(new THREE.Vector3(x, y, z));
  }

  const geometry = new THREE.BufferGeometry().setFromPoints(points);
  const material = new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 2 });
  const line = new THREE.Line(geometry, material);
  scene.add(line);

  // Fade out after a short duration
  setTimeout(() => scene.remove(line), 200);
}

๋‚ ์”จ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ

const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,weather_code...`;
const data = await fetch(url).then(r => r.json());

Openโ€‘Meteo ๋‚ ์”จ ์ฝ”๋“œ๋ฅผ ๋ฌด์„œ์šด ํšจ๊ณผ์— ๋งคํ•‘:

let condition;
if ([95, 96, 99].includes(weatherCode)) condition = "Thunderstorm";
else if ([61, 63, 65].includes(weatherCode)) condition = "Rain";
else if ([71, 73, 75].includes(weatherCode)) condition = "Snow";

์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค

  • ๊ฒ€์ƒ‰์ฐฝ โ€“ ์›ํ•˜๋Š” ๋„์‹œ๋ฅผ ์ฐพ์•„ ๋‚ ์”จ๊ฐ€ ์‚ด์•„๋‚˜๋Š” ๋ชจ์Šต์„ ํ™•์ธํ•˜์„ธ์š”.
  • ํ•˜๋‹จ ๋ฐ” โ€“ ๋น ๋ฅธ ์ ‘๊ทผ ๋ฒ„ํŠผ:
    • ๐Ÿ  ํ™ˆ โ€“ ์นด๋ฉ”๋ผ๋ฅผ ์ดˆ๊ธฐ ์œ„์น˜๋กœ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    • โ„น๏ธ ์ •๋ณด โ€“ ์•ฑ ์ •๋ณด์™€ ํฌ๋ ˆ๋”ง์ด ํฌํ•จ๋œ ๋ชจ๋‹ฌ์„ ์—ฝ๋‹ˆ๋‹ค.
document.getElementById("home-btn").addEventListener("click", () => {
  camera.position.copy(initialCameraPosition);
  controls.target.copy(initialCameraTarget);
  controls.update();
});

ํŒจ๋„์„ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

function togglePanel(panelId) {
  const panel = document.getElementById(panelId);
  panel.classList.toggle("minimized");
}

๋ฌธ์ œ ํ•ด๊ฒฐ

๋ฌธ์ œํ•ด๊ฒฐ์ฑ…
๋ฒˆ๊ฐœ๊ฐ€ โ€œComputed radius is NaNโ€ ์˜ค๋ฅ˜๋ฅผ ์ผ์œผํ‚ด์ขŒํ‘œ๋ฅผ ๊ฒ€์ฆํ•œ ํ›„์— ๊ธฐํ•˜๋ฅผ ์ƒ์„ฑ: `if (isNaN(startX)
๊ธ‰๊ฒฉํ•œ ๋‚ ์”จ ๋ณ€ํ™”๊ฐ€ play()/pause() ์ถฉ๋Œ์„ ์ผ์œผํ‚ดํ˜„์žฌ ์‚ฌ์šด๋“œ๋ฅผ ์ถ”์ ํ•˜๊ณ  ๋‹ค๋ฅธ ์‚ฌ์šด๋“œ๋งŒ ์ผ์‹œ ์ •์ง€: Object.values(weatherSounds).forEach(s => { if (s !== sound) { s.pause(); s.currentTime = 0; } });
๋„์‹œ ๊ฒ€์ƒ‰์ด ์ˆ˜๋™ ๋‚ ์”จ ์„ ํƒ์„ ๋ฎ์–ด์”€์ƒˆ API ๋ฐ์ดํ„ฐ๋ฅผ ํ‘œ์‹œํ•˜๊ธฐ ์ „์— ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ฆฌ์…‹ํ•˜๋Š” manualOverride ํ”Œ๋ž˜๊ทธ ์‚ฌ์šฉ: manualOverride = false; displayWeatherInfo(data);

๊ฒฐ๋ก 

Eerie Weather App์ด ๋ผ์ด๋ธŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋„์‹œ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ , ํญํ’ ๊ฐ•๋„๋ฅผ ๋†’์ด๋ฉฐ, ๋ฒˆ๊ฐœ๊ฐ€ ํ™”๋ฉด์„ ๋น„์ถ”๊ฒŒ ํ•˜์„ธ์š”. Three.js ์˜๊ฐ์„ ์ฐพ๊ณ  ์žˆ๋“ , ๋‚ ์”จ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฐ€์žฅ ๊ทน์ ์ธ ๋ฐฉ๋ฒ•์„ ์›ํ•˜๋“ , ์ด ์•ฑ์ด ๋ชจ๋‘ ํ•ด๊ฒฐํ•ด๋“œ๋ฆฝ๋‹ˆ๋‹ค.

๐ŸŒฉ๏ธ Eerie Weather App ์‹คํ–‰

Back to Blog

๊ด€๋ จ ๊ธ€

๋” ๋ณด๊ธฐ ยป

Reatom: ๋‹น์‹ ๊ณผ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” State Management

์กฐ๊ฐํ™” ๋ฌธ์ œ ํ˜„๋Œ€ ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์—๋Š” ์ต์ˆ™ํ•œ ํŒจํ„ด์ด ์žˆ์Šต๋‹ˆ๋‹ค: - ๊ฐ„๋‹จํ•œ useState ํ›…์œผ๋กœ ์‹œ์ž‘ํ•œ๋‹ค - ๊ณต์œ  ์ƒํƒœ๊ฐ€ ํ•„์š”ํ•˜๋ฉด? Context๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค - Context๊ฐ€ โ€ฆ

React vs Vue vs Svelte โ€” 2025๋…„์— ์–ด๋–ค ๊ฒƒ์„ ๋ฐฐ์›Œ์•ผ ํ• ๊นŒ์š”?

์†Œ๊ฐœ ํ™”๋ฉด์˜ ์ปค์„œ๋Š” ์ปดํ“จํ„ฐ๊ฐ€ ํƒ„์ƒํ•œ ์ด๋ž˜๋กœ ๊ฐ™์€ ์†๋„๋กœ ๊นœ๋ฐ•์ด๊ณ  ์žˆ์ง€๋งŒ, ๊ทธ ๋’ค์— ์žˆ๋Š” ๋ชจ๋“  ๊ฒƒ์€ ๋ณ€ํ–ˆ์Šต๋‹ˆ๋‹ค. There was a t...

JavaScript์˜ ๋น„๋ฐ€์Šค๋Ÿฌ์šด ์‚ถ: โ€˜thisโ€™ ์ดํ•ดํ•˜๊ธฐ

์ œ2์žฅ: Context Is Everything ํ‹ฐ๋ฏธ๋Š” ์‹ ์„ ํ•œ ์ฐจ ํ•œ ์ž”์„ ๋“ค๊ณ  ํ˜ผ๋ž€์Šค๋Ÿฌ์šด ํ‘œ์ •์œผ๋กœ ๋„์„œ๊ด€์— ๋„์ฐฉํ–ˆ๋‹ค. ๊ทธ๋Š” ์•„์นจ ๋‚ด๋‚ด sim์„ debugํ•˜๋ ค๊ณ  ์• ์ผ๋‹ค.