[๐ŸŒŸ] ๋š๋”ฑ! iOS ์ตœ์ข… ํ”„๋กœ์ ํŠธ ์›น ์‚ฌ์ดํŠธ ๋งŒ๋“ค๊ธฐ

Edenยท2025๋…„ 1์›” 19์ผ

React

๋ชฉ๋ก ๋ณด๊ธฐ
4/7

ํŒ€ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด์„œ Family Controls API๋ฅผ ์‹ ์ฒญํ•˜๋Š” ๊ณผ์ •์—์„œ ์šฐ๋ฆฌ ํŒ€์ด ์ œ์ž‘ํ•˜๋ ค๋Š” ์•ฑ์˜ ์›น์‚ฌ์ดํŠธ ๋งํฌ๋ฅผ ์š”๊ตฌ๋ฐ›์•˜๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด์ œ ๋ง‰ ์‹œ์ž‘ํ•œ ํ”„๋กœ์ ํŠธ์—์„œ ์›น ์‚ฌ์ดํŠธ๋Š” ๋‹น์—ฐํžˆ ์—†์ง€ ์•Š์€๊ฐ€. ์–ด๋–ป๊ฒŒ ํ•  ์ง€ ๊ณ ๋ฏผํ•˜๋‹ค๊ฐ€, 4์‹œ๊ฐ„ ์ •๋„๋ฉด ์ถฉ๋ถ„ํžˆ ๋ฐฐํฌ๊นŒ์ง€ ์ง„ํ–‰์ด ๊ฐ€๋Šฅํ•  ๊ฒƒ ๊ฐ™์•„์„œ ์ง์ ‘ ์ง„ํ–‰ํ•˜๋ ค๊ณ  ํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ ํŠœํ„ฐ๋‹˜์ด ๋…ธ์…˜์œผ๋กœ ๊ฐ„๋‹จํžˆ ์ฒ˜๋ฆฌํ•˜๋ผ๊ณ  ์กฐ์–ธํ•ด์ฃผ์…”์„œ ๊ทธ๋ ‡๊ฒŒ ํ–ˆ์ง€๋งŒ, ๋ญ”๊ฐ€ ์•„์‰ฌ์›€์ด ๋‚จ์•˜๋‹ค. ๊ทธ๋ž˜์„œ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ๊ณผ์ •

๐ŸŒŸ ๊ธฐ์ˆ  ์Šคํƒ ์„ ํƒ

CRA๋Š” ์‚ฌ์žฅ๋์œผ๋ฏ€๋กœ Vite๋ฅผ ์‚ฌ์šฉํ•ด React์™€ TypeScript๋กœ ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œํ•ด ์ง„ํ–‰ํ–ˆ๋‹ค. ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ €๋กœ๋Š” pnpm์„ ์‚ฌ์šฉํ–ˆ๋‹ค. ์ดˆ๊ธฐ์—๋Š” Swiper ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด ํ™”๋ฉด์„ ์Šฌ๋ผ์ด๋”๋กœ ๊ตฌํ˜„ํ•˜๋ ค๊ณ  ํ–ˆ์œผ๋‚˜, ์ตœ์ข…์ ์œผ๋กœ๋Š” ํŽ˜์ด์ง€ ๋ผ์šฐํ„ฐ๋กœ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋๋‹ค.

๐ŸŒŸ ๊ตฌ์กฐ ์„ค๊ณ„

React, React-Dom, React-Router-Dom, Styled-Components๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌ์กฐ๋ฅผ ์žก์•˜๋‹ค. iOS๋ฅผ ํ•˜๋ฉด์„œ MVC์™€ MVVM ํŒจํ„ด์„ ์‚ฌ์šฉํ–ˆ๋˜ ํ„ฐ๋ผ, ์˜ค๋žœ๋งŒ์— FE ๊ตฌ์กฐ๋ฅผ ๋‚˜๋ˆ„๋‹ค๋ณด๋‹ˆ ๋งค์šฐ ์–ด์ƒ‰ํ–ˆ๋‹ค. ์ž‘์€ ๊ทœ๋ชจ์˜ ํ”„๋กœ์ ํŠธ๋ผ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŽ์ด ์„ธ๋ถ„ํ™”ํ•˜์ง€๋Š” ์•Š์•˜๊ณ , ํŽ˜์ด์ง€๋ณ„๋กœ ๋‚˜๋ˆ„๊ณ  ๊ฐœ๋ฐœ ์ง„ํ–‰์„ ํ–ˆ๋‹ค. ๋ฉ”์ธ ํŽ˜์ด์ง€๋Š” ํ•˜๋‹จ์— ์•ฑ ๋ชฉ์—…์„ ์ถ”๊ฐ€ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์–ด ๋ณ„๋„๋กœ ์„น์…˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋‚˜๋ˆ„์—ˆ๋‹ค.

๐ŸŒŸ ๋””์ž์ธ ์š”์†Œ

ํ”„๋กœ์ ํŠธ ์ปจ์…‰์ด STAR์ธ ๋งŒํผ ๋ฐคํ•˜๋Š˜์˜ ๋ณ„๊ณผ ์œ ์„ฑ ํšจ๊ณผ๋ฅผ ๋„ฃ๊ณ  ์‹ถ์–ด์„œ ์ด๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.

MeteorEffect.tsx ์ปดํฌ๋„ŒํŠธ

์œ ์„ฑ ํ˜น์€ ๋ณ„๋˜ฅ๋ณ„ ํšจ๊ณผ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋‹ค. ์ฃผ์š” ๊ธฐ๋Šฅ์€ ๋ณ„๋˜ฅ๋ณ„์ฒ˜๋Ÿผ ํ™”๋ฉด์„ ๊ฐ€๋กœ์ง€๋ฅด๋Š” ์š”์†Œ๋“ค์„ ๋žœ๋คํ•œ ์†๋„, ํฌ๊ธฐ, ์ƒ‰์ƒ์œผ๋กœ ์ƒ์„ฑํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๋ฅผ ์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.

import { useEffect, useState } from "react";
import styled, { createGlobalStyle, keyframes } from "styled-components";

// MeteorEffect ์ปดํฌ๋„ŒํŠธ์˜ Props ํƒ€์ž… ์ •์˜
interface MeteorEffectProps {
  count?: number; // ์œ ์„ฑ ๊ฐœ์ˆ˜
  maxDelay?: number; // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์ž‘ ์ „ ์ตœ๋Œ€ ์ง€์—ฐ ์‹œ๊ฐ„
  minSpeed?: number; // ์œ ์„ฑ์˜ ์ตœ์†Œ ์†๋„
  maxSpeed?: number; // ์œ ์„ฑ์˜ ์ตœ๋Œ€ ์†๋„
  angle?: number; // ์œ ์„ฑ์ด ๋–จ์–ด์ง€๋Š” ๊ฐ๋„
  direction?: "left" | "right"; // ์œ ์„ฑ์˜ ๋–จ์–ด์ง€๋Š” ๋ฐฉํ–ฅ
}

const MAX_STAR_COUNT = 50; // ์œ ์„ฑ ๊ฐœ์ˆ˜์˜ ์ตœ๋Œ€๊ฐ’
const colors = ["#c77eff", "#f6ff7e", "#ff8d7e", "#ffffff"]; // ์œ ์„ฑ์˜ ์ƒ‰์ƒ ๋ฐฐ์—ด

const MeteorEffect = ({
  count = 12, // ๊ธฐ๋ณธ ์œ ์„ฑ ๊ฐœ์ˆ˜
  maxDelay = 15, // ๊ธฐ๋ณธ ์ตœ๋Œ€ ์ง€์—ฐ ์‹œ๊ฐ„
  minSpeed = 2, // ๊ธฐ๋ณธ ์ตœ์†Œ ์†๋„
  maxSpeed = 4, // ๊ธฐ๋ณธ ์ตœ๋Œ€ ์†๋„
  angle = 30, // ๊ธฐ๋ณธ ๊ฐ๋„
  direction = "right", // ๊ธฐ๋ณธ ๋ฐฉํ–ฅ
}: MeteorEffectProps) => {
  // ์œ ์„ฑ ๊ฐœ์ˆ˜ ์ œํ•œ
  const starCount = count < MAX_STAR_COUNT ? count : MAX_STAR_COUNT;

  // ์œ ์„ฑ ๊ฐ„ ๊ฐ„๊ฒฉ ์ €์žฅ
  const [starInterval, setStarInterval] = useState<number>(0);

  // ์ฐฝ ํฌ๊ธฐ ๋ณ€ํ™”์— ๋”ฐ๋ผ ์œ ์„ฑ ๊ฐ„ ๊ฐ„๊ฒฉ(starInterval)์„ ๋™์ ์œผ๋กœ ๊ณ„์‚ฐ
  useEffect(() => {
    const calcStarInterval = () => {
      const innerWidth = window.innerWidth;
      setStarInterval(Math.floor((innerWidth * 1.5) / (count * 5)));
    };

    calcStarInterval(); // ์ดˆ๊ธฐ ๊ณ„์‚ฐ
    window.addEventListener("resize", calcStarInterval); // ์ฐฝ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ๋“ฑ๋ก
    return () => {
      window.removeEventListener("resize", calcStarInterval); // ์ด๋ฒคํŠธ ํ•ด์ œ
    };
  }, [count]);

  return (
    <Wrapper> {/* ์œ ์„ฑ ํšจ๊ณผ ์ „์ฒด๋ฅผ ๊ฐ์‹ธ๋Š” ๋ž˜ํผ */}
      <GlobalMeteorStyle $direction={direction} $angle={angle} /> {/* ์ „์—ญ ์Šคํƒ€์ผ ์ ์šฉ */}
      {new Array(starCount).fill(0).map((_, idx) => {
        // ์œ ์„ฑ์˜ ์œ„์น˜, ์• ๋‹ˆ๋ฉ”์ด์…˜ ์†์„ฑ, ์ƒ‰์ƒ ๋“ฑ์„ ๋žœ๋ค์œผ๋กœ ์„ค์ •
        const left =
          direction === "left"
            ? `${Math.random() * count * 5 * starInterval}px`
            : `${window.innerHeight - Math.random() * count * 5 * starInterval}px`;
        const animationDelay = `${Math.random() * maxDelay}s`; // ์ง€์—ฐ ์‹œ๊ฐ„
        const animationDuration =
          maxSpeed > minSpeed
            ? `${minSpeed + Math.random() * (maxSpeed - minSpeed)}s`
            : `${2 + Math.random() * 4}s`; // ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง€์† ์‹œ๊ฐ„
        const colorIndex = Math.floor(Math.random() * colors.length); // ์ƒ‰์ƒ ์„ ํƒ
        const size = `${2 + Math.floor(Math.random() * 5)}px`; // ์œ ์„ฑ ํฌ๊ธฐ
        const boxShadow = `0px 0px 10px 3px ${colors[colorIndex]}`; // ์œ ์„ฑ ๋ฐœ๊ด‘ ํšจ๊ณผ
        return (
          <div
            key={idx}
            style={{ left, animationDelay, animationDuration, boxShadow, width: size, height: size }}
            className="star" // ์œ ์„ฑ ์Šคํƒ€์ผ ์ง€์ •
          ></div>
        );
      })}
    </Wrapper>
  );
};

// ์œ ์„ฑ์ด ๋–จ์–ด์ง€๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ •์˜
const MeteorKeyframe = (direction: "left" | "right", angle: number) => keyframes`
  0% {
    top: -10vh; /* ํ™”๋ฉด ์ƒ๋‹จ ๋ฐ–์—์„œ ์‹œ์ž‘ */
    transform: translateX(0px); /* ์ดˆ๊ธฐ ์œ„์น˜ */
    opacity: 1; /* ์ดˆ๊ธฐ ๋ถˆํˆฌ๋ช…๋„ */
  }
  100% {
    top: 110vh; /* ํ™”๋ฉด ํ•˜๋‹จ ๋ฐ–์œผ๋กœ ์ด๋™ */
    transform: translateX(${direction === "left" ? "-" : "+"}${120 / Math.tan((angle * Math.PI) / 180)}vh);
    opacity: 1; /* ๋ถˆํˆฌ๋ช…๋„ ์œ ์ง€ */
  }
`;

// ์ „์—ญ ์Šคํƒ€์ผ ์ •์˜๋ฅผ ์œ„ํ•œ Props ํƒ€์ž…
interface GlobalMeteorProps {
  $direction: "left" | "right";
  $angle: number;
}

// ์œ ์„ฑ์˜ ์ „์—ญ ์Šคํƒ€์ผ ์ •์˜
const GlobalMeteorStyle = createGlobalStyle<GlobalMeteorProps>`
  .star {
    position: absolute;
    top: -10%; /* ํ™”๋ฉด ์œ„์ชฝ์—์„œ ์‹œ์ž‘ */
    left: 50%; /* ํ™”๋ฉด ์ค‘์•™ ๊ธฐ์ค€ */
    width: 4px; /* ๊ธฐ๋ณธ ํฌ๊ธฐ */
    height: 4px; /* ๊ธฐ๋ณธ ํฌ๊ธฐ */
    border-radius: 50%; /* ์›ํ˜• */
    background-color: white; /* ํฐ์ƒ‰ ๊ธฐ๋ณธ ์ƒ‰์ƒ */
    animation: ${(props) => MeteorKeyframe(props.$direction, props.$angle)} 4s ease-in infinite; /* ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ */
    opacity: 0; /* ์ดˆ๊ธฐ ๋ถˆํˆฌ๋ช…๋„ */
  }
  .star::after {
    position: absolute;
    top: calc(50% - 1px); /* ์ค‘์‹ฌ์ถ• ์„ค์ • */
    left: -950%; /* ๊ผฌ๋ฆฌ ๊ธธ์ด */
    width: 2000%; /* ๊ผฌ๋ฆฌ ๋„ˆ๋น„ */
    height: 2px; /* ๊ผฌ๋ฆฌ ๋‘๊ป˜ */
    background: linear-gradient(to left, #fff0, #ffffff); /* ๊ผฌ๋ฆฌ์˜ ๊ทธ๋ผ๋ฐ์ด์…˜ */
    content: ""; /* ๊ฐ€์ƒ ์š”์†Œ */
    transform: ${(props) =>
      props.$direction === "left"
        ? `rotateZ(-${props.$angle}deg)`
        : `rotateZ(-${180 - props.$angle}deg)`} translateX(50%); /* ํšŒ์ „๊ณผ ์œ„์น˜ ์กฐ์ • */
  }
`;

// ์œ ์„ฑ ํšจ๊ณผ๋ฅผ ๊ฐ์‹ธ๋Š” Wrapper
const Wrapper = styled.div`
  position: fixed; /* ํ™”๋ฉด ๊ณ ์ • */
  top: 0;
  left: 0;
  width: 100%; /* ์ „์ฒด ํ™”๋ฉด ๋„ˆ๋น„ */
  height: 100%; /* ์ „์ฒด ํ™”๋ฉด ๋†’์ด */
  z-index: -10; /* ๋ฐฐ๊ฒฝ์— ์œ„์น˜ */
  pointer-events: none; /* ์ƒํ˜ธ์ž‘์šฉ ๋น„ํ™œ์„ฑํ™” */
`;

export default MeteorEffect;

์ฃผ์š” ๋งค๊ฐœ๋ณ€์ˆ˜

  1. count: ์œ ์„ฑ์˜ ๊ฐœ์ˆ˜๋ฅผ ๊ฒฐ์ •
  2. maxDelay: ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์‹œ์ž‘๋˜๊ธฐ ์ „ ์ง€์—ฐ ์‹œ๊ฐ„์„ ๋žœ๋คํ•˜๊ฒŒ ์ง€์ •
  3. minSpeed, maxSpeed: ์œ ์„ฑ์˜ ์ตœ์†Œ/์ตœ๋Œ€ ์†๋„๋ฅผ ์„ค์ •
  4. angle: ์œ ์„ฑ์ด ๋–จ์–ด์ง€๋Š” ๊ฐ๋„ (๊ธฐ๋ณธ๊ฐ’: 30).
  5. direction: ์œ ์„ฑ์ด ๋–จ์–ด์ง€๋Š” ๋ฐฉํ–ฅ์„ ์„ค์ • ("left" ๋˜๋Š” "right")

์Šคํƒ€์ผ๊ณผ ๋™์ž‘

  1. useEffect
    • ์ฐฝ ํฌ๊ธฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์œ ์„ฑ ๊ฐ„ ๊ฐ„๊ฒฉ(starInterval)์„ ๋™์ ์œผ๋กœ ๊ณ„์‚ฐํ•˜์—ฌ ๋ฐ˜์‘ํ˜• ํšจ๊ณผ ๋งŒ๋“ ๋‹ค.
    • ์ฐฝ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์‹œ ๊ฐ„๊ฒฉ์„ ์žฌ์กฐ์ •ํ•œ๋‹ค.
  2. MeteorKeyframe
    • ์œ ์„ฑ์ด ํ™”๋ฉด์„ ๊ฐ€๋กœ์ง€๋ฅด๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ •์˜ํ•˜๋Š” keyframes
    • angle๊ณผ direction์„ ์‚ฌ์šฉํ•˜์—ฌ ์œ ์„ฑ์˜ ์ด๋™ ๊ฒฝ๋กœ๋ฅผ ๊ณ„์‚ฐ
  3. GlobalMeteorStyle
    • ์œ ์„ฑ์˜ ์Šคํƒ€์ผ๊ณผ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉ
    • ๊ฐ ์œ ์„ฑ์€ box-shadow๋กœ ๋น›๋‚˜๋Š” ํšจ๊ณผ๋ฅผ ํ‘œํ˜„, ::after๋กœ ๊ธด ๊ผฌ๋ฆฌ ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€
  4. Wrapper
    • ์œ ์„ฑ ํšจ๊ณผ์˜ ์ „์ฒด ์˜์—ญ์„ ๊ฐ์‹ธ๋Š” div, ํฌ์ธํ„ฐ ์ด๋ฒคํŠธ๋ฅผ ๋น„ํ™œ์„ฑํ™”(pointer-events: none;)ํ•˜์—ฌ ์ธํ„ฐํŽ˜์ด์Šค์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค.

๋žœ๋คํ•œ ์œ ์„ฑ ์ƒ์„ฑ

  • ์œ ์„ฑ์€ count ๊ฐ’์— ๋”ฐ๋ผ ๊ฐœ์ˆ˜๊ฐ€ ๊ฒฐ์ •๋˜๋ฉฐ, Math.random()์„ ์‚ฌ์šฉํ•ด ๋‹ค์Œ์„ ๋žœ๋คํ•˜๊ฒŒ ์„ค์ •ํ•œ๋‹ค.
    • ์œ„์น˜ (left): ๋ฐฉํ–ฅ์— ๋”ฐ๋ผ ์‹œ์ž‘ ์œ„์น˜๊ฐ€ ๋‹ค๋ฆ„.
    • ์ง€์—ฐ ์‹œ๊ฐ„ (animationDelay): ์œ ์„ฑ์ด ๋‚˜ํƒ€๋‚˜๋Š” ๊ฐ„๊ฒฉ.
    • ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ๊ฐ„ (animationDuration): ์†๋„.
    • ํฌ๊ธฐ์™€ ์ƒ‰์ƒ (size, boxShadow): ์œ ์„ฑ์˜ ํฌ๊ธฐ์™€ ๋น›๊น”.

๊ฒฐ๊ณผ

  • ํ™”๋ฉด์— ๋ฐ˜์‘ํ˜• ์œ ์„ฑ ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๋™์ ์ธ ์‹œ๊ฐ์  ๊ฒฝํ—˜์„ ์ œ๊ณต
  • ์œ ์„ฑ์˜ ๋ฐฉํ–ฅ๊ณผ ์†๋„, ๊ฐœ์ˆ˜ ๋“ฑ์„ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์‰ฝ๊ฒŒ ์กฐ์ • ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ๋‹ค์–‘ํ•œ ๋””์ž์ธ ์š”๊ตฌ์— ์œ ์—ฐํ•˜๊ฒŒ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋‹ค.




StarryNight.tsx ์ปดํฌ๋„ŒํŠธ

๋ณ„ ํšจ๊ณผ๋ฅผ ์œ„ํ•œ StarryNight ์ปดํฌ๋„ŒํŠธ๋„ ๊ตฌํ˜„ํ–ˆ๋‹ค. ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฐ˜์‘ํ˜•์œผ๋กœ ์ฐฝ ํฌ๊ธฐ์— ๋”ฐ๋ผ ๋ณ„ ๊ฐœ์ˆ˜๋ฅผ ์กฐ์ •ํ•œ๋‹ค.

import { useEffect, useState } from "react";
import styled, { keyframes } from "styled-components";

const StarryNight = () => {
  const [stars, setStars] = useState<JSX.Element[]>([]);

  // ๋ณ„์„ ์ƒ์„ฑํ•˜์—ฌ ํ™”๋ฉด์— ๋ Œ๋”๋ง
  const makeStars = () => {
    const maxSize = Math.max(window.innerWidth, window.innerHeight); // ํ™”๋ฉด ํฌ๊ธฐ ๊ณ„์‚ฐ
    const starCount = Math.floor(maxSize / 2); // ๋ณ„ ๊ฐœ์ˆ˜ ๊ฒฐ์ •

    const getRandomX = () => Math.random() * maxSize; // ๋žœ๋ค x์ขŒํ‘œ ์ƒ์„ฑ
    const getRandomY = () => Math.random() * maxSize; // ๋žœ๋ค y์ขŒํ‘œ ์ƒ์„ฑ
    const randomRadius = () => Math.random() * 0.7 + 0.6; // ๋ณ„ ๋ฐ˜์ง€๋ฆ„ ๋žœ๋ค ์„ค์ •

    const newStars = Array.from({ length: starCount }, (_, index) => (
      <Star key={index} cx={getRandomX()} cy={getRandomY()} r={randomRadius()} />
    ));

    setStars(newStars);
  };

  useEffect(() => {
    makeStars();

    const handleResize = () => {
      makeStars();
    };

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return (
    <BackSky>
      <Sky>{stars}</Sky>
    </BackSky>
  );
};

// ๋ณ„ ํ•˜๋Š˜ ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ •์˜
const moveStar = keyframes`
  from {
    transform: translate(-50%, -50%) rotate(0);
  }
  to {
    transform: translate(-50%, -50%) rotate(360deg);
  }
`;

const BackSky = styled.div`
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  z-index: -1;
`;

const Sky = styled.svg`
  width: 100vw;
  height: 100vw;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  animation: ${moveStar} 480s linear infinite;
`;

const Star = styled.circle`
  fill: #fff;
`;

export default StarryNight;

ํ•ต์‹ฌ ๊ธฐ๋Šฅ

  1. ๋ณ„ ์ƒ์„ฑ (makeStars)
    • ํ™”๋ฉด์˜ ํฌ๊ธฐ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ณ„์˜ ๊ฐœ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•˜๋ฉฐ, ๋žœ๋คํ•œ ์ขŒํ‘œ์™€ ํฌ๊ธฐ๋ฅผ ๊ฐ€์ง„ ๋ณ„์„ ์ƒ์„ฑ
    • ๊ฐ๊ฐ์˜ ๋ณ„์€ SVG์˜ <circle> ์š”์†Œ๋กœ ๊ตฌํ˜„
  2. ๋ฐ˜์‘ํ˜• ์ง€์›
    • window์˜ resize ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์ง€ํ•˜์—ฌ ํ™”๋ฉด ํฌ๊ธฐ๊ฐ€ ๋ณ€ํ•  ๋•Œ ๋ณ„๋“ค์„ ๋‹ค์‹œ ์ƒ์„ฑ
    • ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์–‘ํ•œ ํ™”๋ฉด ํฌ๊ธฐ์—์„œ๋„ ๋ณ„์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ‘œ์‹œ
  3. ๋ณ„ ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜
    • ๋ณ„ ํ•˜๋Š˜ ์ „์ฒด๋ฅผ ์ฒœ์ฒœํžˆ ํšŒ์ „์‹œํ‚ค๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ •์˜. (moveStar)

makeStars

๋ณ„๋“ค์„ ์ƒ์„ฑํ•˜๊ณ  useState๋ฅผ ํ†ตํ•ด ์ƒํƒœ๋กœ ๊ด€๋ฆฌํ•œ๋‹ค. ๋žœ๋คํ•œ ์ขŒํ‘œ์™€ ํฌ๊ธฐ๋ฅผ ๊ฐ€์ง€๋Š” ๋ณ„๋“ค๋กœ ์ด๋ฃจ์–ด์ง„ ๋ฐฐ์—ด์„ ์ƒ์„ฑํ•œ๋‹ค.

const makeStars = () => {
  const maxSize = Math.max(window.innerWidth, window.innerHeight); // ๊ฐ€์žฅ ํฐ ํ™”๋ฉด ํฌ๊ธฐ ๊ณ„์‚ฐ
  const starCount = Math.floor(maxSize / 2); // ๋ณ„์˜ ๊ฐœ์ˆ˜๋Š” ํ™”๋ฉด ํฌ๊ธฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒฐ์ •

  const getRandomX = () => Math.random() * maxSize; // ๋žœ๋ค X ์ขŒํ‘œ
  const getRandomY = () => Math.random() * maxSize; // ๋žœ๋ค Y ์ขŒํ‘œ
  const randomRadius = () => Math.random() * 0.7 + 0.6; // ๋žœ๋ค ๋ฐ˜์ง€๋ฆ„

  const newStars = Array.from({ length: starCount }, (_, index) => (
    <Star key={index} cx={getRandomX()} cy={getRandomY()} r={randomRadius()} />
  ));

  setStars(newStars); // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
};

useEffect

useEffect๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ฒ˜์Œ ๋ Œ๋”๋ง๋  ๋•Œ ๋ณ„์„ ์ƒ์„ฑํ•˜๋ฉฐ, resize ์ด๋ฒคํŠธ๋ฅผ ๋“ฑ๋กํ•˜์—ฌ ํ™”๋ฉด ํฌ๊ธฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋„ ๋ณ„์„ ๋‹ค์‹œ ์ƒ์„ฑํ•˜๋„๋ก ํ•จ.

useEffect(() => {
  makeStars();

  const handleResize = () => {
    makeStars(); // ํ™”๋ฉด ํฌ๊ธฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ ๋ณ„ ๋‹ค์‹œ ์ƒ์„ฑ
  };

  window.addEventListener("resize", handleResize); // ์ด๋ฒคํŠธ ๋“ฑ๋ก

  return () => {
    window.removeEventListener("resize", handleResize); // ์ด๋ฒคํŠธ ํ•ด์ œ
  };
}, []);

์Šคํƒ€์ผ๊ณผ ์• ๋‹ˆ๋ฉ”์ด์…˜

  • BackSky: ๋ณ„ ํšจ๊ณผ๊ฐ€ ๋ฐฐ๊ฒฝ์œผ๋กœ ๋™์ž‘ํ•˜๋„๋ก ํ•˜๋Š” div. ์ „์ฒด ํ™”๋ฉด์„ ๋ฎ๊ณ  ๋‹ค๋ฅธ ์ฝ˜ํ…์ธ ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋„๋ก ์„ค์ •
  • Sky: ๋ณ„๋“ค์ด ํšŒ์ „ ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ํ•จ๊ป˜ ๋ Œ๋”๋ง๋˜๋Š” SVG ์š”์†Œ. transform ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ค‘์•™ ๊ธฐ์ค€ ํšŒ์ „ ํšจ๊ณผ๋ฅผ ์ ์šฉ
  • Star: ๊ฐœ๋ณ„ ๋ณ„์„ ๋‚˜ํƒ€๋‚ด๋Š” <circle>๋กœ, ํฐ์ƒ‰์œผ๋กœ ๋ Œ๋”๋ง๋จ

์• ๋‹ˆ๋ฉ”์ด์…˜ ์ •์˜

moveStar๋Š” ํ•˜๋Š˜ ์ „์ฒด๊ฐ€ ํšŒ์ „ํ•˜๋Š” ํšจ๊ณผ๋ฅผ ๋งŒ๋“ ๋‹ค. ์• ๋‹ˆ๋ฉ”์ด์…˜ ์†๋„๋Š” 480์ดˆ๋กœ ์„ค์ •ํ•˜์—ฌ ๋งค์šฐ ์ฒœ์ฒœํžˆ ์›€์ง์ด๋„๋ก ํ•œ๋‹ค.

const moveStar = keyframes`
  from {
    transform: translate(-50%, -50%) rotate(0); // ์ดˆ๊ธฐ ์ƒํƒœ
  }
  to {
    transform: translate(-50%, -50%) rotate(360deg); // ํ•œ ๋ฐ”ํ€ด ํšŒ์ „
  }
`;

๐ŸŒŸ ์ „์ฒด ๊ตฌ์กฐ

App.tsx์—์„œ MeteorEffect์™€ StarryNight๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๋ฉ”์ธ ํŽ˜์ด์ง€๋ฅผ ๊ตฌ์„ฑํ–ˆ๋‹ค.

import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Header from "./components/layout/Header";
import Home from "./pages/Home";
import Vision from "./pages/Vision";
import Feature from "./pages/Feature";
import Team from "./pages/Team";
import Footer from "./components/layout/Footer";
import { ThemeProvider } from "styled-components";
import GlobalStyle from "./styles/GlobalStyle";
import StarryNight from "./components/common/StarryNight";
import MeteorEffect from "./components/common/MeteorEffect";
import theme from "./styles/Theme";

function App() {
  return (
    <ThemeProvider theme={theme}>
      <GlobalStyle />
      <Router>
        <StarryNight />
        <MeteorEffect count={20} angle={45} direction="right" />
        <Header />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/vision" element={<Vision />} />
          <Route path="/feature" element={<Feature />} />
          <Route path="/team" element={<Team />} />
        </Routes>
        <Footer />
      </Router>
    </ThemeProvider>
  );
}

export default App;

์ „์ฒด ๊ตฌ์กฐ์— ์Šคํƒ€์ผ๋“œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ThemeProvider์™€ GlobalStyle์„ ์ ์šฉํ•ด์ฃผ๊ณ  ๋””์ž์ธ ์š”์†Œ๋ฅผ importํ•œ ๋’ค Header์™€ Footer๊ฐ€ ํ•ญ์ƒ ๋ณด์ผ ์ˆ˜ ์žˆ๋„๋ก ๊ทธ ์‚ฌ์ด์— ํŽ˜์ด์ง€ Routes๋กœ ๊ตฌ์„ฑํ•œ ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

๐ŸŒŸ ์™„์„ฑ

STAR ์›น์‚ฌ์ดํŠธ

๋žœ๋ค์œผ๋กœ ๋–จ์–ด์ง€๋Š” ์˜ˆ์œ ์œ ์„ฑ๋“ค๊ณผ, ์ฒœ์ฒœํžˆ ๋Œ์•„๊ฐ€๋Š” ๋ฐคํ•˜๋Š˜์ด ์•„๋ฆ„๋‹ค์šด S T A R์˜ ์›น ์‚ฌ์ดํŠธ๊ฐ€ ์™„์„ฑ๋๋‹ค.

์ด ํ”„๋กœ์ ํŠธ๋Š” ๊ฐ„๋‹จํžˆ ๋งŒ๋“ค์—ˆ์ง€๋งŒ, ์‹ค์ œ ๋ฐฐํฌ์— ํ•„์š”ํ•œ ์ˆ˜์ค€์œผ๋กœ ์ œ์ž‘ํ–ˆ๋‹ค. ํŠนํžˆ, ์ œ๋กœ๋ฒ ์ด์Šค์—์„œ ์‹œ์ž‘ํ•ด ๋””์ž์ธ ์š”์†Œ์™€ ํšจ๊ณผ์— ์ง‘์ค‘ํ•ด ํ”„๋กœ์ ํŠธ์˜ ์ปจ์…‰์„ ์‹œ๊ฐ์ ์œผ๋กœ ์ž˜ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋…ธ๋ ฅํ–ˆ๋‹ค. API ์Šน์ธ ๋ฐ›๊ณ  ์–ผ๋ฅธ ์•ฑ๊นŒ์ง€ ๊ตฌํ˜„ ํ•ด๋‚ด๊ณ  ์‹ถ๋‹ค.

์œ ์„ฑ ๋ฐฐ๊ฒฝํšจ๊ณผ๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๊ธฐ
[JS] ๋ฐคํ•˜๋Š˜์˜ ๋ณ„ ๋ฐฐ๊ฒฝ ๋งŒ๋“ค๊ธฐ

profile
์•ˆ๋…•ํ•˜์„ธ์š”? ์ด๋“ ์ž…๋‹ˆ๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€