๐ŸŒฉ๏ธ Haunted Weather App ๋งŒ๋“ค๊ธฐ: 3D Web Development์˜ ์œผ์Šค์Šคํ•œ ์—ฌ์ •

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

Source: Dev.to

์†Œ๊ฐœ

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

๋””์ž์ธ

์ƒ‰์ƒ ๊ตฌ์„ฑ

  • ๋ฐฐ๊ฒฝ: #1a1a2e (๊นŠ์€ ํŒŒ๋ž€โ€‘๋ณด๋ผ) โ€“ ํญํ’ ์ „ ํ•˜๋Š˜๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  • ๊ฐ•์กฐ: #460809 โ†’ #f4320b (hover ์‹œ) โ€“ ํ”ผ์ฒ˜๋Ÿผ ๋ถ‰์€ ๊ฐ•์กฐ์ƒ‰.
  • ํ…์ŠคํŠธ ๊ธ€๋กœ์šฐ: ๋งฅ๋™ํ•˜๋Š” ํ…์ŠคํŠธ ๊ทธ๋ฆผ์ž๋ฅผ ๊ฐ€์ง„ ์—ํ…Œ๋ฆฌ์–ผ ๋ ˆ๋“œ.
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 ๋ฐฐ๊ฒฝ์œผ๋กœ ์Šคํƒ€์ผ๋ง๋ฉ๋‹ˆ๋‹ค.

/* Example background image */
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);      // ๋‹ฌ๋น› ๋ฒ ์ด์Šค
const directionalLight = new THREE.DirectionalLight(0xaaaadd, 0.3); // ๋ถ€๋“œ๋Ÿฌ์šด ๋‹ฌ๋น›
const rimLight = new THREE.DirectionalLight(0x6666aa, 0.2);          // ์€์€ํ•œ ์—ญ๊ด‘

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

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";

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

  • ๊ฒ€์ƒ‰ ๋ฐ” โ€“ ์›ํ•˜๋Š” ๋„์‹œ๋ฅผ ์ฐพ์•„ ๋‚ ์”จ๊ฐ€ ์‚ด์•„๋‚˜๋Š” ๋ชจ์Šต์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • ํ•˜๋‹จ ๋ฐ” โ€“ ๋น ๋ฅธ ์ ‘๊ทผ ๋ฒ„ํŠผ:
    • ๐Ÿ  Home โ€“ ์นด๋ฉ”๋ผ๋ฅผ ์ดˆ๊ธฐ ์œ„์น˜๋กœ ๋ฆฌ์…‹ํ•ฉ๋‹ˆ๋‹ค.
    • โ„น๏ธ About โ€“ ์•ฑ ์ •๋ณด์™€ ํฌ๋ ˆ๋”ง์ด ๋‹ด๊ธด ๋ชจ๋‹ฌ์„ ์—ฝ๋‹ˆ๋‹ค.
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

๊ด€๋ จ ๊ธ€

๋” ๋ณด๊ธฐ ยป

๋ชจ๋“ˆ ๋ชจ๋“œ

๊ธฐ๋ณธ ๊ตฌ์กฐ ๋ชจ๋“ˆ ํŒจํ„ด์€ IIFE์™€ ํด๋กœ์ €(Closure)์˜ ํŠน์„ฑ์„ ์ด์šฉํ•ด โ€œํด๋ž˜์Šคโ€ ๊ฐœ๋…์„ ๋ชจ๋ฐฉํ•˜๊ณ , ๊ณต๊ฐœ(Public)์™€ ๋น„๊ณต๊ฐœ(Private) ๋ฉค๋ฒ„ ๋ฐ ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ JavaScript์—์„œ ์บก์Аํ™”(Encapsulation)๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ณ ์ „์ ์ธ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค. ์ฝ”๋“œ ์˜ˆ์‹œ: ํ•˜๋‚˜์˜ ์นด์šดํ„ฐ ๋ชจ๋“ˆ javascript var CounterModuleโ€ฆ

[Boost]

๐Ÿš€ ํด๋ผ์ด์–ธํŠธ ์ธก vs ์„œ๋ฒ„ ์ธก CORS: ์‹ค์ œ ์ฐจ์ด์  ์ดํ•ดํ•˜๊ธฐ Shanthi's Dev Diary โ€ข Dec 4 Tags: webdev, cors, javascript, node...

React Testing Pipeline ์ดํ•ดํ•˜๊ธฐ (์ดˆ๋ณด์ž๋ฅผ ์œ„ํ•œ)

์†Œ๊ฐœ React๋ฅผ React Testing Library(RTL)๋กœ ํ…Œ์ŠคํŠธํ•  ๋•Œ, ๋‚ด๋ถ€์—์„œ ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚˜๊ณ  ์žˆ๋Š”์ง€ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ๋„์›€์ด ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ๋Š” ํŒŒ์ดํ”„๋ผ์ธ์„ ์ž์„ธํžˆ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹คโ€”...