FrontEnd Develop

๐Ÿ“Š Timer ํ™œ์šฉํ•œ ๊ทธ๋ž˜ํ”„ ์ƒ์Šน ์• ๋‹ˆ๋ฉ”์ด์…˜ ์•„์ด๋””์–ด ๊ฐœ๋ฐœ

Frisbeen 2025. 1. 31. 01:12

๐Ÿš€ React์—์„œ useEffect๋ฅผ ํ™œ์šฉํ•œ ๊ทธ๋ž˜ํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ๋ฒ•

 

https://www.loom.com/share/c80fd4298f6d420e9d182623ded33997?sid=56b1c360-1dd4-427a-8208-ac537fe408a0

 

recharts๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„(BarChart)๊ฐ€ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์œ„์—์„œ ์•„๋ž˜๋กœ ์†Ÿ์•„์˜ค๋ฅด๋Š” ํšจ๊ณผ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ‘‰ ํ•ต์‹ฌ ํฌ์ธํŠธ๋Š” ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ(0 ๊ฐ’)๋ฅผ useState๋กœ ์„ค์ •ํ•˜๊ณ , useEffect๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ’์„ ์ ์ง„์ ์œผ๋กœ ์ฆ๊ฐ€์‹œํ‚ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ๋ฉ”์ปค๋‹ˆ์ฆ˜์™€ ์•„์ด๋””์–ด ๊ทธ๋ฆฌ๊ณ  ์ƒํ™ฉ

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

 

๋ณด์—ฌ์ง€๋Š” ์‹œ๊ฐ์  ๊ทธ๋ž˜ํ”„๊ฐ€ 0 -> (๊ฐ–๊ณ ์žˆ๋Š” ๋ฐ์ดํ„ฐ์˜ ๊ฐ’) ์œผ๋กœ ์ƒ์Šนํ•˜๋Š” ํšจ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํ–ฅ์ƒํ•˜์ง€ ์•Š์„๊นŒ ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋•Œ timer ํ™œ์šฉ ๋ฐ zero ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ํ•˜๋‚˜ ๋” ์ถ”๊ฐ€ํ•œ ํ›„, zero ๋”๋ฏธ ๋ฐ์ดํ„ฐ -> ๋‚˜์˜ ๋ฐ์ดํ„ฐ ๋ณด์—ฌ์ฃผ๋ฉด ๋  ๊ฒƒ ๊ฐ™์•˜์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ๊ธฐ๋Œ€ํšจ๊ณผ

 

๐Ÿ”น ์ฒ˜์Œ ๊ทธ๋ž˜ํ”„๊ฐ€ ๋ Œ๋”๋ง๋  ๋•Œ ๋ชจ๋“  ๋ง‰๋Œ€๊ฐ€ 0์—์„œ ์‹œ์ž‘ 

๐Ÿ”น useEffect๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋ฉฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ

๐Ÿ”น ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์œ„์—์„œ ์•„๋ž˜๋กœ ์†Ÿ์•„์˜ค๋ฅด๋Š” ๊ทธ๋ž˜ํ”„ ๊ตฌํ˜„

๐Ÿ”น ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์ฆ๊ฐ€

 

๐ŸŽฏ ์™„์„ฑ๋œ ์ฝ”๋“œ (GraphPage.jsx) <์นœ์ ˆํ•œ ์ฃผ์„์„ ๊ณ๋“ค์ธ>

import { useContext, useEffect, useState } from 'react';
import {
  BarChart,
  Bar,
  XAxis,
  YAxis,
  Tooltip,
  ResponsiveContainer,
  Legend,
  CartesianGrid,
  Cell,
} from 'recharts';
import { GoalContext } from '../context/GoalContext';
import '../style/GraphPage.scss';
import { SidebarContext } from '../context/SidebarContext';

const GraphPage = () => {
  const { goalAmount } = useContext(GoalContext);
  const { isSidebarOpen } = useContext(SidebarContext);

  // ์ฃผ์š” ์†Œ๋น„ ํ•ญ๋ชฉ (๋”๋ฏธ ๋ฐ์ดํ„ฐ)
  const expenseItems = [
    { category: '์นดํŽ˜ & ์Œ๋ฃŒ', item: '์Šคํƒ€๋ฒ…์Šค ๋ผ๋–ผ', amount: 5_000 },
    { category: '์‹์‚ฌ', item: '์ ์‹ฌ ์‹์‚ฌ (๊น€์น˜์ฐŒ๊ฐœ)', amount: 12_000 },
    { category: '์ƒํ™œ์šฉํ’ˆ', item: '์„ธ์ œ ๊ตฌ์ž…', amount: 8_000 },
    { category: '์—ฌ๊ฐ€ ํ™œ๋™', item: '๋„ทํ”Œ๋ฆญ์Šค ๊ตฌ๋…', amount: 14_000 },
    { category: '๊ธฐํƒ€', item: '์ฑ… ๊ตฌ์ž…', amount: 20_000 },
  ];

  const totalSpending = expenseItems.reduce((acc, cur) => acc + cur.amount, 0);

  // โœ… ์ดˆ๊ธฐ ๊ทธ๋ž˜ํ”„ ๋ฐ์ดํ„ฐ (๋ชจ๋“  ๊ฐ’ 0)
  const [animatedData, setAnimatedData] = useState([
    { label: '์ด ์ง€์ถœ', value: 0 },
    { label: '์ด ์†Œ๋น„', value: 0 },
    { label: '๋ชฉํ‘œ ๊ธˆ์•ก', value: 0 },
  ]);

  // โœ… ์‹ค์ œ ๊ทธ๋ž˜ํ”„ ๋ฐ์ดํ„ฐ
  const originalData = [
    { label: '์ด ์ง€์ถœ', value: totalSpending },
    { label: '์ด ์†Œ๋น„', value: 40_000 },
    { label: '๋ชฉํ‘œ ๊ธˆ์•ก', value: goalAmount || 0 },
  ];

  // โœ… `useEffect`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ
  useEffect(() => {
    // 1๏ธโƒฃ `setTimeout`์„ ์ด์šฉํ•ด ๋น„๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ (์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ)
    const timeout = setTimeout(() => {
      // 2๏ธโƒฃ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ ์šฉ๋  ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธ
      setAnimatedData(originalData);
    }, 300); // 0.3์ดˆ ํ›„ ์‹คํ–‰ (๋ถ€๋“œ๋Ÿฌ์šด ์‹œ์ž‘ ํšจ๊ณผ)

    // 3๏ธโƒฃ `useEffect` ์ •๋ฆฌ ํ•จ์ˆ˜: ๋ถˆํ•„์š”ํ•œ `setTimeout` ํด๋ฆฌ์–ด (๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ)
    return () => clearTimeout(timeout);
  }, [goalAmount]); // ๐Ÿ”„ `goalAmount`๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์‹คํ–‰

  return (
    <div className={`graph-wrapper ${isSidebarOpen ? 'sidebar-active' : ''}`}>
      <div className="graph-left">
        <div className="expense-graph-card">
          <h3>์ง€์ถœ, ์†Œ๋น„, ๋ชฉํ‘œ ๊ธˆ์•ก ๋น„๊ต ๊ทธ๋ž˜ํ”„</h3>
          <ResponsiveContainer width="100%" height={300}>
            <BarChart data={animatedData} barGap={10}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="label" tick={{ fontSize: 14 }} />
              <YAxis tick={{ fontSize: 14 }} />
              <Tooltip
                contentStyle={{
                  backgroundColor: 'white',
                  border: 'none',
                  boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.1)',
                }}
                itemStyle={{ color: '#001f5c', fontWeight: 600 }}
              />
              <Legend verticalAlign="top" height={36} />
              <Bar dataKey="value" fill="#001f5c" radius={[10, 10, 0, 0]}>
                {animatedData.map((entry, index) => (
                  <Cell key={`cell-${index}`} style={{ transition: 'height 1s ease-in-out' }} />
                ))}
              </Bar>
            </BarChart>
          </ResponsiveContainer>
        </div>
      </div>
    </div>
  );
};

export default GraphPage;

 

 

๐Ÿ“Œ ํ•ต์‹ฌ ์ฝ”๋“œ ์„ค๋ช… (useEffect ์• ๋‹ˆ๋ฉ”์ด์…˜ ์›๋ฆฌ)

useEffect(() => {
  const timeout = setTimeout(() => {
    setAnimatedData(originalData); // โœ… 0์—์„œ ๋ชฉํ‘œ ๊ฐ’์œผ๋กœ ๋ณ€๊ฒฝ
  }, 300); // 0.3์ดˆ ํ›„ ์‹คํ–‰ (๋ถ€๋“œ๋Ÿฌ์šด ์‹œ์ž‘ ํšจ๊ณผ)

  return () => clearTimeout(timeout); // โœ… ๋ถˆํ•„์š”ํ•œ ์‹คํ–‰ ๋ฐฉ์ง€ (๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ)
}, [goalAmount]); // ๐Ÿ”„ `goalAmount` ๋ณ€๊ฒฝ ์‹œ ์‹คํ–‰

 

 

โœ… ํ˜น์‹œ๋‚˜ ํ•˜๋Š” ๋งˆ์Œ์—... goalAmount๊ฐ€ useEffect ์˜์กด์„ฑ ๋ฐฐ์—ด์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”๊ฐ€? (๊ทธ๋ฆฌ๊ณ  goalAmount๋Š” ๋ฌด์—‡์ธ๊ฐ€?)

 

goalAmount๋Š” ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ(Context API)๋ฅผ ํ†ตํ•ด ๊ฐ€์ ธ์˜ค๋Š” ์‚ฌ์šฉ์ž์˜ ๋ชฉํ‘œ๊ธˆ์•ก์ž…๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ๋ชฉํ‘œ ๊ธˆ์•ก์„ ๋ณ€๊ฒฝํ•˜๋ฉด ๊ทธ๋ž˜ํ”„๋„ ์—…๋ฐ์ดํŠธ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

๋˜ํ•œ, ์‚ฌ์šฉ์ž๊ฐ€ ๋ชฉํ‘œ ๊ธˆ์•ก์„ ๋ฐ”๊พผ๋‹ค๋ฉด ๊ทธ์— ๋”ฐ๋ผ ๊ทธ๋ž˜ํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋‹ค์‹œ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ”น ์˜ˆ์ œ ์‹œ๋‚˜๋ฆฌ์˜ค

1. ์‚ฌ์šฉ์ž๊ฐ€ ๋ชฉํ‘œ ๊ธˆ์•ก์„ 100,000์›์—์„œ 200,000์›์œผ๋กœ ๋ณ€๊ฒฝ

2. goalAmount ๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด์„œ useEffect๊ฐ€ ์‹คํ–‰

 

 

์ถ”๊ฐ€) originalData ๋ฐฐ์—ด์ด goalAmount๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ

โ€ข originalData๋Š” ์ตœ์ข…์ ์œผ๋กœ ๊ทธ๋ž˜ํ”„์— ํ‘œ์‹œํ•  ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

โ€ข ํ•˜์ง€๋งŒ originalData ๋‚ด๋ถ€์— goalAmount๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ,

goalAmount๊ฐ€ ๋ฐ”๋€Œ๋ฉด originalData๋„ ๋ฐ”

const originalData = [
  { label: '์ด ์ง€์ถœ', value: totalSpending },
  { label: '์ด ์ˆ˜์ž…', value: 40_000 },
  { label: '๋ชฉํ‘œ ๊ธˆ์•ก', value: goalAmount || 0 }, // โœ… goalAmount ์‚ฌ์šฉ
];

 

โ€ข ๋งŒ์•ฝ ์˜์กด์„ฑ ๋ฐฐ์—ด์— goalAmount๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชฉํ‘œ ๊ธˆ์•ก์ด ๋ณ€๊ฒฝ๋˜์–ด๋„ ๊ทธ๋ž˜ํ”„๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

โ€ข ์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ๋ชฉํ‘œ ๊ธˆ์•ก์„ 300,000์› โ†’ 500,000์›์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์„ ๋•Œ,

useEffect๊ฐ€ ์‹คํ–‰๋˜์ง€ ์•Š์œผ๋ฉด ๊ทธ๋ž˜ํ”„๋Š” ์—ฌ์ „ํžˆ 300,000์›์œผ๋กœ ๋‚จ์•„์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

๋ชฉํ‘œ ๊ธˆ์•ก ๋ณ€๊ฒฝ ์‹œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด (useEffect์˜ ์ฒ ํ•™)

โ€ข ๊ธฐ์กด ๊ฐ’์ด ๋ฐ”๋กœ ๋ณ€๊ฒฝ๋˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ ์šฉํ•˜๋ฉด์„œ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

โ€ข goalAmount๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค setAnimatedData๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ์˜์กด์„ฑ ๋ฐฐ์—ด์— ํฌํ•จํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค

 

 

๐Ÿš€ useEffect๋ฅผ ํ™œ์šฉํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ตฌํ˜„ ๊ณผ์ •

 

1๏ธโƒฃ ์ดˆ๊ธฐ ๊ฐ’ ์„ค์ • (useState)

const [animatedData, setAnimatedData] = useState([
  { label: '์ด ์ง€์ถœ', value: 0 },
  { label: '์ด ์†Œ๋น„', value: 0 },
  { label: '๋ชฉํ‘œ ๊ธˆ์•ก', value: 0 },
]);

โ€ข ๋ชจ๋“  ๊ทธ๋ž˜ํ”„์˜ ๊ฐ’์ด 0์—์„œ ์‹œ์ž‘

โ€ข ๊ทธ๋ž˜ํ”„๊ฐ€ ํ•œ ๋ฒˆ์— ๋‚˜ํƒ€๋‚˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ์ ์ง„์ ์œผ๋กœ ์ฆ๊ฐ€ํ•˜๋„๋ก ์„ค์ •

 

2๏ธโƒฃ useEffect๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ

useEffect(() => {
  const timeout = setTimeout(() => {
    setAnimatedData(originalData);
  }, 300);
  return () => clearTimeout(timeout);
}, [goalAmount]);

 

๐Ÿ”น ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ(0) โ†’ ๋ชฉํ‘œ ๋ฐ์ดํ„ฐ(์‹ค์ œ ๊ฐ’)๋กœ ๋ณ€๊ฒฝ

๐Ÿ”น setTimeout์„ ํ™œ์šฉํ•˜์—ฌ 0.3์ดˆ ๋’ค ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธ

๐Ÿ”น return () => clearTimeout(timeout);์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ์‹คํ–‰์„ ๋ฐฉ์ง€

 

 

3๏ธโƒฃ Cell์„ ํ™œ์šฉํ•œ ๋ง‰๋Œ€ ๊ทธ๋ž˜ํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜

<Bar dataKey="value" fill="#001f5c" radius={[10, 10, 0, 0]}>
  {animatedData.map((entry, index) => (
    <Cell key={`cell-${index}`} style={{ transition: 'height 1s ease-in-out' }} />
  ))}
</Bar>

๐Ÿ”น ๊ฐ **๋ง‰๋Œ€(Bar)**์— ๊ฐœ๋ณ„์ ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ ์ ์šฉ

๐Ÿ”น transition: height 1s ease-in-out;์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์˜ฌ๋ผ์˜ค๋Š” ํšจ๊ณผ

 

 

๐ŸŽฏ ๊ฒฐ๊ณผ

 

๐Ÿ”ฅ ์™„์„ฑ๋œ ๊ทธ๋ž˜ํ”„๋Š” ์ดˆ๊ธฐ์—๋Š” 0์—์„œ ์‹œ์ž‘ํ•œ ํ›„, ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์œ„๋กœ ์˜ฌ๋ผ์˜ค๋ฉฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ๊ฐ€ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค!

โœ” useEffect๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ Œ๋”๋ง ํ›„ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ

โœ” setTimeout์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ณ€ํ•˜๋Š” ๋Š๋‚Œ์„ ์คŒ

โœ” Cell์— transition์„ ์ ์šฉํ•˜์—ฌ ๊ทธ๋ž˜ํ”„๊ฐ€ ์œ„๋กœ ์†Ÿ์•„์˜ค๋ฅด๋Š” ํšจ๊ณผ ์ถ”๊ฐ€