๊ณต์๋ฌธ์์ ์๋ challenges
์ ๊ณตํ๋ sandbox์์ ์ค์ต ํ ๊ธฐ๋กํ๋ค.
1. useCounter
// App.js
import { useCounter } from "./useCounter";
export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}
// useCounter.js
import { useState, useEffect } from "react";
export function useCounter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return count;
}
2. Make the counter delay configurable
์ฌ๋ผ์ด๋๋ก ์กฐ์ ํ๋ ์ง์ฐ ๊ฐ์ ์ปค์คํ
ํ
์ ์ ๋ฌํ๊ณ , 1000ms๋ฅผ ํ๋์ฝ๋ฉํ๋ ๋์ ์ ๋ฌ๋ ์ง์ฐ์ ์ฌ์ฉํ๋๋ก ์นด์ดํฐ ํ
์ ๋ณ๊ฒฝํด๋ณด์.
import { useState } from 'react';
import { useCounter } from './useCounter.js';
export default function Counter() {
const [delay, setDelay] = useState(1000);
const count = useCounter(delay);
return (
<>
<label>
Tick duration: {delay} ms
<br />
<input
type="range"
value={delay}
min="10"
max="2000"
onChange={e => setDelay(Number(e.target.value))}
/>
</label>
<hr />
<h1>Ticks: {count}</h1>
</>
);
}
import { useState, useEffect } from "react";
export function useCounter(delay) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => c + 1);
}, delay);
return () => clearInterval(id);
}, [delay]);
return count;
}
ํ์ฌ ์ฌ์ฉ ์ค์ธ ์นด์ดํฐ ํ
์ ๋ ๊ฐ์ง ์์
์ ์ํํ๋ค.
๊ฐ๊ฒฉ์ ์ค์ ํ๊ณ ๊ฐ๊ฒฉ์ด ํฑ๋ ๋๋ง๋ค ์ํ ๋ณ์๋ฅผ ์ฆ๊ฐ์ํจ๋ค.
๊ฐ๊ฒฉ์ ์ค์ ํ๋ ๋ก์ง์ useInterval์ด๋ผ๋ ๋ณ๋์ Hook์ผ๋ก ๋ถ๋ฆฌํ์ธ์. ์ด Hook์ ๋ ๊ฐ์ ์ธ์๋ฅผ ๋ฐ์์ผ ํฉ๋๋ค:
onTick callback ๋ฐ delay.
// App.js
import { useCounter } from "./useCounter.js";
export default function Counter() {
const count = useCounter(1000);
return <h1>Seconds passed: {count}</h1>;
}
//useCounter.js
import { useState } from "react";
import { useInterval } from "./useInterval.js";
export function useCounter(delay) {
const [count, setCount] = useState(0);
useInterval(() => {
setCount((c) => c + 1);
}, delay);
return count;
}
// useInterval.js
import { useEffect } from "react";
export function useInterval(onTick, delay) {
useEffect(() => {
const id = setInterval(onTick, delay);
return () => clearInterval(id);
}, [onTick, delay]);
}
์๋๋ useCounter์์ useEffect๋ก ์ฒ๋ฆฌํ๋๋ฐ
setInterval์ ๋ฐ๋ก ๋นผ๋ ค๊ณ ํ๋ฉด useEffect ์งธ๋ก ๋นผ์ค์ผ ํจ.
ํ
๊ท์น์ ์ํด ์ด๋ฐ ๊ตฌ์กฐ์์๋ useEffect๊ฐ ๋งจ ํ์๋ก ๊ฐ์ผํ๋ค.
4. Fix a resetting interval
์ด ์์์๋ ๋ ๊ฐ์ ๊ฐ๋ณ ๊ฐ๊ฒฉ์ด ์์ต๋๋ค.
App ์ปดํฌ๋ํธ๋ useCounter๋ฅผ ํธ์ถํ๊ณ , ์ด ์ปดํฌ๋ํธ๋ useInterval์ ํธ์ถํ์ฌ ๋งค์ด๋ง๋ค ์นด์ดํฐ๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.
๊ทธ๋ฌ๋ App ์ปดํฌ๋ํธ๋ ๋ํ useInterval์ ํธ์ถํ์ฌ 2์ด๋ง๋ค ํ์ด์ง ๋ฐฐ๊ฒฝ์์ ์์๋ก ์
๋ฐ์ดํธํฉ๋๋ค.
์ด๋ค ์ด์ ์์์ธ์ง ํ์ด์ง ๋ฐฐ๊ฒฝ์ ์
๋ฐ์ดํธํ๋ ์ฝ๋ฐฑ์ด ์คํ๋์ง ์์ต๋๋ค. ์ฌ์ฉ ๊ฐ๊ฒฉ ์์ ๋ช ๊ฐ์ง ๋ก๊ทธ๋ฅผ ์ถ๊ฐํฉ๋๋ค:
useEffect(() => {
console.log('โ
Setting up an interval with delay ', delay)
const id = setInterval(onTick, delay);
return () => {
console.log('โ Clearing an interval with delay ', delay)
clearInterval(id);
};
}, [onTick, delay]);
๋ก๊ทธ๊ฐ ์์ํ ๊ฒ๊ณผ ์ผ์นํ๋์? ์ผ๋ถ ์ดํํธ๊ฐ ๋ถํ์ํ๊ฒ ์ฌ๋๊ธฐํ๋๋ ๊ฒ ๊ฐ๋ค๋ฉด ์ด๋ค ์ข
์์ฑ ๋๋ฌธ์ ๊ทธ๋ฐ ์ผ์ด ๋ฐ์ํ๋์ง ์ง์ํ ์ ์๋์? ์ดํํธ์์ ํด๋น ์ข
์์ฑ์ ์ ๊ฑฐํ ์ ์๋ ๋ฐฉ๋ฒ์ด ์๋์?
๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ํ์๋ ํ์ด์ง ๋ฐฐ๊ฒฝ์ด 2์ด๋ง๋ค ์
๋ฐ์ดํธ๋ ๊ฒ์ผ๋ก ์์๋ฉ๋๋ค.
ํํธ : ์ฌ์ฉ์ค์ธ Interval Hook์ด ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ธ์๋ก ๋ฐ์๋ค์ด๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ Effect์ ์ข
์์ฑ์ด ๋ ํ์๊ฐ ์๋๋ก ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๊ฐ์ธ๋ ๋ฐฉ๋ฒ์ ์๊ฐํด๋ผ ์ ์์๊น์?
useInterval ๋ด์์ ํฑ ์ฝ๋ฐฑ์ effect event์ ๋ํํฉ๋๋ค.
์ด๋ ๊ฒ ํ๋ฉด Effect์ ์ข
์์ฑ์์ onTick์ ์๋ตํ ์ ์์ต๋๋ค.
์ปดํฌ๋ํธ๋ฅผ ๋ค์ ๋ ๋๋งํ ๋๋ง๋ค Effect๊ฐ ๋ค์ ๋๊ธฐํ๋์ง ์์ผ๋ฏ๋ก
ํ์ด์ง ๋ฐฐ๊ฒฝ์ ๋ณ๊ฒฝ ๊ฐ๊ฒฉ์ด ๋งค์ด๋ง๋ค ์ฌ์ค์ ๋์ง ์์ต๋๋ค.
์ด ๋ณ๊ฒฝ์ผ๋ก ๋ ๊ฐ๊ฒฉ์ด ๋ชจ๋ ์์๋๋ก ์๋ํ๋ฉฐ ์๋ก ๊ฐ์ญํ์ง ์์ต๋๋ค:
// App.js
import { useCounter } from './useCounter.js';
import { useInterval } from './useInterval.js';
export default function Counter() {
const count = useCounter(1000);
useInterval(() => {
const randomColor = `hsla(${Math.random() * 360}, 100%, 50%, 0.2)`;
document.body.style.backgroundColor = randomColor;
}, 2000);
return <h1>Seconds passed: {count}</h1>;
}
// useCounter.js
import { useState } from 'react';
import { useInterval } from './useInterval.js';
export function useCounter(delay) {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(c => c + 1);
}, delay);
return count;
}
// ๋ฐ๊พธ๊ธฐ ์ useInterval.js
import { useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
export function useInterval(onTick, delay) {
useEffect(() => {
const id = setInterval(onTick, delay);
return () => {
clearInterval(id);
};
}, [onTick, delay]);
}
// useInterval.js
import { useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
export function useInterval(callback, delay) {
const onTick = useEffectEvent(callback)
useEffect(() => {
const id = setInterval(onTick, delay);
return () => {
clearInterval(id);
};
},[delay]);
}
callback์ useEffectEvent๋ฅผ ์ด์ฉํด ์์กด์ฑ ๋ฐฐ์ด์์ ๋นผ์คฌ๋๋ฐ,
useEffectEvent ์ฌ์ฉํ์ง์๊ณ ๊ทธ๋ฅ ์์กด์ฑ ๋ฐฐ์ด์์ onTick ๋นผ๊ธฐ๋งํด๋ ์๋ํ๋ค.
useEffectEvent๋ ์์ง stable version์์๋ ์ฌ์ฉํ ์ ์๋ค.
๋น๋ฐ์ ๋ก์ง์ ์ดํํธ ์ด๋ฒคํธ๋ก ์ถ์ถํ ์ ์๋ hook์ด๋ผ๋ ์ค๋ช
์ด ์๋๋ฐ,
๊ทธ๋ฅ ์์กด์ฑ ๋ฐฐ์ด์์ ๋บ๊ฒ๊ณผ ์ ํํ ์ด๋ป๊ฒ ๋ค๋ฅธ์ง๋ ์์ง ๋ชจ๋ฅด๊ฒ ๋ค.
์๋ฌดํผ ์ ์ฝ๋์์๋ ์์กด์ฑ ๋ฐฐ์ด์ ๋ฆฌ๋ ๋ ๋ ์กฐ๊ฑด์ด ์๋ ์ฝ๋ฐฑํจ์๊น์ง ํฌํจ๋์ด ์์๋ค๋๊ฒ ํต์ฌ์ผ๋ก ๋ณด์ธ๋ค.
5. Implement a staggering movement
์ด ์์์์ usePointerPosition
Hook์ ํ์ฌ ํฌ์ธํฐ ์์น๋ฅผ ์ถ์ ํฉ๋๋ค.
์ปค์๋ ์๊ฐ๋ฝ์ ์์ญ ์๋ก ์ด๋ํ๋ฉด ๋นจ๊ฐ์ ์ ์ด ์์ง์์ ๋ฐ๋ผ๊ฐ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๊ทธ ์์น๋ pos1 ๋ณ์์ ์ ์ฅ๋ฉ๋๋ค.
์ค์ ๋ก๋ ๋ค์ฏ ๊ฐ(!)์ ๋ค๋ฅธ ๋นจ๊ฐ์ ์ ์ด ๋ ๋๋ง๋๊ณ ์์ต๋๋ค.
ํ์ฌ๋ ๋ชจ๋ ๊ฐ์ ์์น์ ๋ํ๋๊ธฐ ๋๋ฌธ์ ๋ณด์ด์ง ์์ต๋๋ค. ์ด ๋ถ๋ถ์ ์์ ํด์ผ ํฉ๋๋ค.
๋์ ๊ตฌํํ๋ ค๋ ๊ฒ์ "์๊ฐ๋ฆฐ" ์์ง์์
๋๋ค.
๊ฐ ์ ์ด ์ด์ ์ ์ ๊ฒฝ๋ก๋ฅผ "๋ฐ๋ผ์ผ" ํฉ๋๋ค.
์๋ฅผ ๋ค์ด ์ปค์๋ฅผ ๋น ๋ฅด๊ฒ ์ด๋ํ๋ฉด ์ฒซ ๋ฒ์งธ ์ ์ ์ฆ์ ๋ฐ๋ผ๊ฐ๊ณ , ๋ ๋ฒ์งธ ์ ์ ์ฝ๊ฐ์ ์ง์ฐ์ ๋๊ณ ์ฒซ ๋ฒ์งธ ์ ์ ๋ฐ๋ผ๊ฐ๊ณ ,
์ธ ๋ฒ์งธ ์ ์ ๋ ๋ฒ์งธ ์ ์ ๋ฐ๋ผ๊ฐ๋ ์์ผ๋ก ์์ฐจ๋ฅผ ๋์ด์ผ ํฉ๋๋ค.
์ฌ์ฉ ์ง์ฐ๋ ๊ฐ ์ฌ์ฉ์ ์ ์ Hook์ ๊ตฌํํด์ผ ํฉ๋๋ค.
ํ์ฌ ๊ตฌํ์ ์ ๊ณต๋ ๊ฐ์ ๋ฐํํฉ๋๋ค. ๋์ ๋ฐ๋ฆฌ์ด ์ ์ง์ฐ์์ ๊ฐ์ ๋ค์ ๋ฐํํ๊ณ ์ถ์ต๋๋ค.
์ด๋ฅผ ์ํด์๋ state์ Effect๊ฐ ํ์ํ ์ ์์ต๋๋ค.
์ฌ์ฉ ์ง์ฐ๋ ๊ฐ์ ๊ตฌํํ๊ณ ๋๋ฉด ์ ๋ค์ด ์๋ก ๋ฐ๋ผ ์์ง์ด๋ ๊ฒ์ ๋ณผ ์ ์์ ๊ฒ์
๋๋ค.
// ๊ธฐ์กด App.js
import { usePointerPosition } from './usePointerPosition.js';
function useDelayedValue(value, delay) {
// TODO: Implement this Hook
return value;
}
export default function Canvas() {
const pos1 = usePointerPosition();
const pos2 = useDelayedValue(pos1, 100);
const pos3 = useDelayedValue(pos2, 200);
const pos4 = useDelayedValue(pos3, 100);
const pos5 = useDelayedValue(pos3, 50);
return (
<>
<Dot position={pos1} opacity={1} />
<Dot position={pos2} opacity={0.8} />
<Dot position={pos3} opacity={0.6} />
<Dot position={pos4} opacity={0.4} />
<Dot position={pos5} opacity={0.2} />
</>
);
}
function Dot({ position, opacity }) {
return (
<div style={{
position: 'absolute',
backgroundColor: 'pink',
borderRadius: '50%',
opacity,
transform: `translate(${position.x}px, ${position.y}px)`,
pointerEvents: 'none',
left: -20,
top: -20,
width: 40,
height: 40,
}} />
);
}
// usePointerPosition
import { useState, useEffect } from 'react';
export function usePointerPosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
useEffect(() => {
function handleMove(e) {
setPosition({ x: e.clientX, y: e.clientY });
}
window.addEventListener('pointermove', handleMove);
return () => window.removeEventListener('pointermove', handleMove);
}, []);
return position;
}
// App.js
import { useState, useEffect } from 'react';
import { usePointerPosition } from './usePointerPosition.js';
function useDelayedValue(value, delay) {
const [delayedValue, setDelayedValue] = useState(value);
useEffect(() => {
setTimeout(() => {
setDelayedValue(value);
}, delay);
}, [value, delay]);
return delayedValue;
}
export default function Canvas() {
const pos1 = usePointerPosition();
const pos2 = useDelayedValue(pos1, 100);
const pos3 = useDelayedValue(pos2, 200);
const pos4 = useDelayedValue(pos3, 100);
const pos5 = useDelayedValue(pos3, 50);
return (
<>
<Dot position={pos1} opacity={1} />
<Dot position={pos2} opacity={0.8} />
<Dot position={pos3} opacity={0.6} />
<Dot position={pos4} opacity={0.4} />
<Dot position={pos5} opacity={0.2} />
</>
);
}
function Dot({ position, opacity }) {
return (
<div style={{
position: 'absolute',
backgroundColor: 'pink',
borderRadius: '50%',
opacity,
transform: `translate(${position.x}px, ${position.y}px)`,
pointerEvents: 'none',
left: -20,
top: -20,
width: 40,
height: 40,
}} />
);
}
์ฐ์์ ์ผ๋ก ์๋..
์ด ํจ๊ณผ๋ cleanup์ด ํ์ ์๋ค.
cleanup์์ clearTimeout
์ ํธ์ถํ๋ฉด ๊ฐ์ด ์ด๋ฏธ ์์ฝ๋ ํ์์์์ด ์ฌ์ค์ ๋๋ค.
๋์์ ๊ณ์ ์ ์งํ๋ ค๋ฉด ๋ชจ๋ ํ์์์์ด ์คํ๋๋๋ก ํด์ผ ํ๋ค.