React Hooks ์์ ์ ๋ณต
React 16.8์์ ๋์ ๋ Hooks๋ ํจ์ํ ์ปดํฌ๋ํธ์์๋ State์ ์๋ช ์ฃผ๊ธฐ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์๊ฒ ํด์ค ํ์ ์ ์ธ ๋ณํ๋ค. ์ด ๊ธ์์๋ ์ฃผ์ Hooks ์ ๋ถ๋ฅผ ์ค์ ์์ ์ ํจ๊ป ๊น๊ฒ ์ดํด๋ณธ๋ค.
1. Hooks๋? ์ ๋ฑ์ฅํ๋?
Hooks๊ฐ ๋ฑ์ฅํ๊ธฐ ์ , React๋ ํด๋์คํ ์ปดํฌ๋ํธ์ ํจ์ํ ์ปดํฌ๋ํธ ๋ ๊ฐ์ง๊ฐ ์์๋ค. ํจ์ํ ์ปดํฌ๋ํธ๋ ๋จ์ํ๊ณ ๊ฐ๋ณ์ง๋ง, State ๊ด๋ฆฌ๋ ์๋ช ์ฃผ๊ธฐ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ์ ์์๋ค. ๋ณต์กํ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ค๋ฉด ๋ฐ๋์ ํด๋์ค ์ปดํฌ๋ํธ๋ฅผ ์จ์ผ ํ๋ค.
ํด๋์คํ ์ปดํฌ๋ํธ์ ๋ฌธ์ ์
// ํด๋์คํ ์ปดํฌ๋ํธ โ ๋ณต์กํ๊ณ ์ฅํฉํ๋ค
class UserProfile extends React.Component {
constructor(props) {
super(props)
this.state = {
user: null,
loading: true,
error: null,
}
this.handleClick = this.handleClick.bind(this) // this ๋ฐ์ธ๋ฉ ํ์
}
componentDidMount() {
this.fetchUser()
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser()
}
}
componentWillUnmount() {
// ํด๋ฆฐ์
๋ก์ง
}
fetchUser() {
fetch(`/api/users/${this.props.userId}`)
.then(res => res.json())
.then(user => this.setState({ user, loading: false }))
.catch(error => this.setState({ error, loading: false }))
}
handleClick() {
// this ๋ฐ์ธ๋ฉ ์์ผ๋ฉด ์๋ฌ
}
render() {
const { user, loading, error } = this.state
if (loading) return <p>๋ก๋ฉ ์ค...</p>
if (error) return <p>์๋ฌ ๋ฐ์</p>
return <div>{user.name}</div>
}
}ํด๋์คํ ์ปดํฌ๋ํธ์ ์ฃผ์ ๋ฌธ์ :
this๋ฐ์ธ๋ฉ ๋ฌธ์ ๋ก ์ธํ ๋ฒ๊ทธ- ์๋ช
์ฃผ๊ธฐ ๋ฉ์๋์ ๊ด๋ จ ์๋ ๋ก์ง์ด ์์ (
componentDidMount์ ์ฌ๋ฌ API ํธ์ถ) - ๋ก์ง ์ฌ์ฌ์ฉ์ด ์ด๋ ค์ (HOC, render props ํจํด์ด ๋ณต์กํจ)
- ํด๋์ค ์์ฒด๊ฐ JavaScript ์ด๋ณด์์๊ฒ ๋ฏ์ ๊ฐ๋
Hooks๋ก ๋ณํํ๋ฉด
// ํจ์ํ ์ปดํฌ๋ํธ + Hooks โ ํจ์ฌ ๊ฐ๊ฒฐํ๋ค
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
setLoading(true)
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(user => { setUser(user); setLoading(false) })
.catch(error => { setError(error); setLoading(false) })
}, [userId]) // userId๊ฐ ๋ฐ๋ ๋๋ง๋ค ์ฌ์คํ
if (loading) return <p>๋ก๋ฉ ์ค...</p>
if (error) return <p>์๋ฌ ๋ฐ์</p>
return <div>{user.name}</div>
}ํจ์ฌ ์งง๊ณ ์ง๊ด์ ์ด๋ค. ์ด๊ฒ Hooks์ ํ์ด๋ค.
2. useState ์ฌํ
useState๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ Hook์ด์ง๋ง ์์์ผ ํ ์ธ๋ถ ์ฌํญ์ด ์๋ค.
์ง์ฐ ์ด๊ธฐํ(Lazy Initialization)
์ด๊ธฐ๊ฐ ๊ณ์ฐ์ด ๋ฌด๊ฑฐ์ธ ๋, ๋งค ๋ ๋๋ง๋ง๋ค ๊ณ์ฐํ์ง ์๋๋ก ํจ์๋ฅผ ์ ๋ฌํ ์ ์๋ค.
// ๋งค ๋ ๋๋ง๋ง๋ค ์คํ๋จ (๋ถํ์ํ ๊ณ์ฐ)
const [data, setData] = useState(heavyCalculation())
// ์ง์ฐ ์ด๊ธฐํ: ์ต์ด ๋ ๋๋ง์๋ง ์คํ๋จ
const [data, setData] = useState(() => heavyCalculation())
// ์ค์ ์์: localStorage์์ ์ด๊ธฐ๊ฐ ์ฝ๊ธฐ
const [theme, setTheme] = useState(() => {
const saved = localStorage.getItem('theme')
return saved || 'light'
})ํจ์ํ ์ ๋ฐ์ดํธ
์ด์ State ๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ์ ๋ฐ์ดํธํ ๋๋ ํจ์ํ ์ ๋ฐ์ดํธ๋ฅผ ์ฌ์ฉํ๋ค.
function Counter() {
const [count, setCount] = useState(0)
// ํจ์ํ ์
๋ฐ์ดํธ: ํญ์ ์ต์ state ๊ฐ์ ๋ณด์ฅ
const increment = () => setCount(prev => prev + 1)
const decrement = () => setCount(prev => prev - 1)
const double = () => setCount(prev => prev * 2)
const reset = () => setCount(0)
// ์ฌ๋ฌ ๋ฒ ์ฐ์ ์
๋ฐ์ดํธ ์ ํจ์ํ์ด ์์
const incrementThree = () => {
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// ๊ฒฐ๊ณผ: ์ ํํ 3 ์ฆ๊ฐ
}
return (
<div>
<p>์นด์ดํธ: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={double}>x2</button>
<button onClick={incrementThree}>+3</button>
<button onClick={reset}>์ด๊ธฐํ</button>
</div>
)
}3. useEffect ์์ ์ ๋ณต
useEffect๋ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋ ํ์ ์คํ๋๋ ์ฌ์ด๋ ์ดํํธ๋ฅผ ์ฒ๋ฆฌํ๋ค. API ํธ์ถ, ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก, DOM ์ง์ ์กฐ์ ๋ฑ์ ์ฌ์ฉํ๋ค.
์์กด์ฑ ๋ฐฐ์ด์ ์ธ ๊ฐ์ง ํํ
// 1. ์์กด์ฑ ๋ฐฐ์ด ์์: ๋งค ๋ ๋๋ง ํ๋ง๋ค ์คํ
useEffect(() => {
console.log('๋งค๋ฒ ์คํ')
})
// 2. ๋น ๋ฐฐ์ด: ์ต์ด ๋ง์ดํธ ์ ํ ๋ฒ๋ง ์คํ
useEffect(() => {
console.log('๋ง์ดํธ ์ ํ ๋ฒ๋ง')
fetchInitialData()
}, [])
// 3. ์์กด์ฑ ์ง์ : ํด๋น ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์คํ
useEffect(() => {
console.log(`userId๊ฐ ${userId}๋ก ๋ณ๊ฒฝ๋จ`)
fetchUserData(userId)
}, [userId])ํด๋ฆฐ์ (Cleanup) ํจ์
useEffect๋ ๋ฐํ๊ฐ์ผ๋ก ํด๋ฆฐ์
ํจ์๋ฅผ ๋ฐ์ ์ ์๋ค. ์ปดํฌ๋ํธ๊ฐ ์ธ๋ง์ดํธ๋๊ฑฐ๋ ๋ค์ effect๊ฐ ์คํ๋๊ธฐ ์ ์ ํธ์ถ๋๋ค.
function ChatRoom({ roomId }) {
useEffect(() => {
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก
const socket = connectToRoom(roomId)
socket.on('message', handleMessage)
console.log(`๋ฐฉ ${roomId}์ ์ฐ๊ฒฐ๋จ`)
// ํด๋ฆฐ์
: ์ธ๋ง์ดํธ๋๊ฑฐ๋ roomId๊ฐ ๋ฐ๋๊ธฐ ์ ์ ์คํ
return () => {
socket.off('message', handleMessage)
socket.disconnect()
console.log(`๋ฐฉ ${roomId}์์ ์ฐ๊ฒฐ ํด์ `)
}
}, [roomId])
return <div>์ฑํ
๋ฐฉ: {roomId}</div>
}ํด๋ฆฐ์ ์ด ํ์ํ ๊ฒฝ์ฐ:
- ์ด๋ฒคํธ ๋ฆฌ์ค๋ (
addEventListenerโremoveEventListener) - ํ์ด๋จธ (
setIntervalโclearInterval) - ์์ผ ์ฐ๊ฒฐ ํด์
- ๋น๋๊ธฐ ์์ฒญ ์ทจ์ (
AbortController)
๋น๋๊ธฐ ํจ์์ useEffect
useEffect์ ์ฝ๋ฐฑ์ ์ง์ async๋ก ๋ง๋ค ์ ์๋ค. ๋ด๋ถ์ async ํจ์๋ฅผ ์ ์ํด์ ํธ์ถํด์ผ ํ๋ค.
// ์๋ชป๋ ์ฝ๋ (๊ฒฝ๊ณ ๋ฐ์)
useEffect(async () => {
const data = await fetchData()
setData(data)
}, [])
// ์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ 1: ๋ด๋ถ์ async ํจ์ ์ ์
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`)
const data = await response.json()
setUser(data)
} catch (error) {
setError(error.message)
} finally {
setLoading(false)
}
}
fetchUser()
}, [userId])
// ์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ 2: AbortController๋ก ์์ฒญ ์ทจ์ ์ฒ๋ฆฌ
useEffect(() => {
const controller = new AbortController()
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
})
const data = await response.json()
setUser(data)
} catch (error) {
if (error.name !== 'AbortError') {
setError(error.message)
}
}
}
fetchUser()
return () => controller.abort() // ์ปดํฌ๋ํธ ์ธ๋ง์ดํธ ์ ์์ฒญ ์ทจ์
}, [userId])๋ฌดํ ๋ฃจํ ์ฃผ์
useEffect ์์์ State๋ฅผ ๋ณ๊ฒฝํ๊ณ , ๊ทธ State๊ฐ ์์กด์ฑ ๋ฐฐ์ด์ ์์ผ๋ฉด ๋ฌดํ ๋ฃจํ๊ฐ ๋ฐ์ํ๋ค.
// ๋ฌดํ ๋ฃจํ ๋ฐ์!
const [count, setCount] = useState(0)
useEffect(() => {
setCount(count + 1) // count๋ฅผ ๋ณ๊ฒฝํ๋ฉด ๋ฆฌ๋ ๋๋ง โ ๋ค์ effect ์คํ โ ๋ฌดํ๋ฐ๋ณต
}, [count])
// ํด๊ฒฐ: ํจ์ํ ์
๋ฐ์ดํธ๋ก ์์กด์ฑ ์ ๊ฑฐ
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1) // count ์์ด๋ ์์ ํ๊ฒ ์ฆ๊ฐ
}, 1000)
return () => clearInterval(timer)
}, []) // count๊ฐ ์์กด์ฑ ๋ฐฐ์ด์ ์์ด๋ ๋จ4. useRef: DOM ์ฐธ์กฐ์ ๊ฐ ์ ์ง
useRef๋ ๋ ๊ฐ์ง ์ฉ๋๋ก ์ฌ์ฉํ๋ค: DOM ์๋ฆฌ๋จผํธ ์ง์ ์ ๊ทผ, ๋ฆฌ๋ ๋๋ง ์์ด ๊ฐ ์ ์ง.
DOM ์ฐธ์กฐ
function TextInputWithFocus() {
const inputRef = useRef(null)
const focusInput = () => {
inputRef.current.focus() // DOM ์๋ฆฌ๋จผํธ์ ์ง์ ์ ๊ทผ
}
const clearInput = () => {
inputRef.current.value = ''
inputRef.current.focus()
}
return (
<div>
<input ref={inputRef} type="text" placeholder="์ฌ๊ธฐ์ ์
๋ ฅ..." />
<button onClick={focusInput}>ํฌ์ปค์ค</button>
<button onClick={clearInput}>์ง์ฐ๊ธฐ</button>
</div>
)
}๋ฆฌ๋ ๋๋ง ์์ด ๊ฐ ์ ์ง
useRef๋ .current ๊ฐ์ด ๋ณํด๋ ์ปดํฌ๋ํธ๋ฅผ ๋ฆฌ๋ ๋๋งํ์ง ์๋๋ค. ํ์ด๋จธ ID๋ ์ด์ props ๊ฐ์ ์ ์ฅํ ๋ ์ ์ฉํ๋ค.
function StopWatch() {
const [time, setTime] = useState(0)
const [isRunning, setIsRunning] = useState(false)
const intervalRef = useRef(null) // ํ์ด๋จธ ID ์ ์ฅ (๋ฆฌ๋ ๋๋ง ๋ถํ์)
const start = () => {
if (isRunning) return
setIsRunning(true)
intervalRef.current = setInterval(() => {
setTime(prev => prev + 1)
}, 1000)
}
const stop = () => {
clearInterval(intervalRef.current)
setIsRunning(false)
}
const reset = () => {
clearInterval(intervalRef.current)
setIsRunning(false)
setTime(0)
}
return (
<div>
<p>{time}์ด</p>
<button onClick={start} disabled={isRunning}>์์</button>
<button onClick={stop} disabled={!isRunning}>์ ์ง</button>
<button onClick={reset}>๋ฆฌ์
</button>
</div>
)
}์ด์ ๊ฐ ์ ์ฅ (previous value)
function usePrevious(value) {
const prevRef = useRef()
useEffect(() => {
prevRef.current = value // ๋ ๋๋ง ํ ์ด์ ๊ฐ ์ ์ฅ
})
return prevRef.current // ํ์ฌ ๋ ๋๋ง์์๋ ์ด์ ๊ฐ ๋ฐํ
}
function PriceTracker({ price }) {
const prevPrice = usePrevious(price)
const diff = price - (prevPrice || price)
const direction = diff > 0 ? 'โฒ' : diff < 0 ? 'โผ' : 'โ'
const color = diff > 0 ? 'red' : diff < 0 ? 'blue' : 'black'
return (
<div>
<span>ํ์ฌ ๊ฐ๊ฒฉ: {price.toLocaleString()}์</span>
<span style={{ color }}>
{direction} {Math.abs(diff).toLocaleString()}์
</span>
</div>
)
}5. useMemo: ๊ณ์ฐ ๊ฒฐ๊ณผ ์บ์ฑ
useMemo๋ ๋ณต์กํ ๊ณ์ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ๋ชจ์ด์ ์ด์
ํ๋ค. ์์กด์ฑ ๊ฐ์ด ๋ฐ๋์ง ์์ผ๋ฉด ์ด์ ๊ณ์ฐ ๊ฒฐ๊ณผ๋ฅผ ์ฌ์ฌ์ฉํ๋ค.
import { useState, useMemo } from 'react'
function ProductList({ products, searchQuery, category }) {
// ํํฐ๋ง + ์ ๋ ฌ โ ๋น์ฉ์ด ํฐ ์ฐ์ฐ
const filteredProducts = useMemo(() => {
console.log('ํํฐ๋ง ์ฐ์ฐ ์คํ')
return products
.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase())
const matchesCategory = category === 'all' || p.category === category
return matchesSearch && matchesCategory
})
.sort((a, b) => a.price - b.price)
}, [products, searchQuery, category]) // ์ด ์ธ ๊ฐ์ด ๋ฐ๋ ๋๋ง ์ฌ๊ณ์ฐ
const totalPrice = useMemo(() => {
return filteredProducts.reduce((sum, p) => sum + p.price, 0)
}, [filteredProducts])
return (
<div>
<p>์ด {filteredProducts.length}๊ฐ / ํฉ๊ณ: {totalPrice.toLocaleString()}์</p>
<ul>
{filteredProducts.map(p => (
<li key={p.id}>{p.name} - {p.price.toLocaleString()}์</li>
))}
</ul>
</div>
)
}useMemo ์ฌ์ฉ ๊ธฐ์ค
useMemo๋ฅผ ํญ์ ์ฐ๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ์ง๋ง, ๊ณผ์ฉํ๋ฉด ์คํ๋ ค ์ฑ๋ฅ์ด ๋๋น ์ง๋ค.
์ฌ์ฉํด์ผ ํ ๋:
- ๋ฆฌ์คํธ ํํฐ๋ง/์ ๋ ฌ์ฒ๋ผ ๋ฐฐ์ด ์ฐ์ฐ์ด ๋ฌด๊ฑฐ์ธ ๋
- ๊ณ์ฐ ๊ฒฐ๊ณผ๊ฐ ๋ค๋ฅธ Hook์ ์์กด์ฑ์ผ๋ก ์ฌ์ฉ๋ ๋
- React DevTools Profiler๋ก ์ค์ ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ํ์ธ๋์์ ๋
์ฌ์ฉํ์ง ์์๋ ๋ ๋:
- ๋จ์ํ ์ฌ์น์ฐ์ฐ
- ์งง์ ๋ฐฐ์ด(10๊ฐ ์ดํ) ์ฒ๋ฆฌ
- ์ปดํฌ๋ํธ๊ฐ ์์ฃผ ๋ฆฌ๋ ๋๋ง๋์ง ์์ ๋
6. useCallback: ํจ์ ๋ฉ๋ชจ์ด์ ์ด์
useCallback์ ํจ์๋ฅผ ๋ฉ๋ชจ์ด์ ์ด์
ํ๋ค. ์์กด์ฑ์ด ๋ฐ๋์ง ์์ผ๋ฉด ๊ฐ์ ํจ์ ์ฐธ์กฐ๋ฅผ ์ ์งํ๋ค.
import { useState, useCallback, memo } from 'react'
// React.memo๋ก ์ต์ ํ๋ ์์ ์ปดํฌ๋ํธ
const Button = memo(({ onClick, label }) => {
console.log(`Button "${label}" ๋ ๋๋ง`)
return <button onClick={onClick}>{label}</button>
})
function Parent() {
const [count, setCount] = useState(0)
const [text, setText] = useState('')
// useCallback ์์ด โ ๋งค ๋ ๋๋ง๋ง๋ค ์ ํจ์ ์์ฑ
// โ Button์ด props ๋ณ๊ฒฝ์ผ๋ก ์ธ์ํด์ ๋งค๋ฒ ๋ฆฌ๋ ๋๋ง
const badIncrement = () => setCount(c => c + 1)
// useCallback ์ฌ์ฉ โ ํจ์ ์ฐธ์กฐ ์ ์ง
// โ ์์กด์ฑ์ด ๋ฐ๋์ง ์์ผ๋ฉด Button์ด ๋ฆฌ๋ ๋๋ง๋์ง ์์
const increment = useCallback(() => {
setCount(c => c + 1)
}, []) // ์์กด์ฑ ์์ โ ํญ์ ๊ฐ์ ํจ์
const decrement = useCallback(() => {
setCount(c => c - 1)
}, [])
return (
<div>
<p>์นด์ดํธ: {count}</p>
<input value={text} onChange={e => setText(e.target.value)} />
<Button onClick={increment} label="์ฆ๊ฐ" />
<Button onClick={decrement} label="๊ฐ์" />
</div>
)
}React.memo + useCallback ์กฐํฉ์ ์ค์ ํจ๊ณผ
// ๋ฆฌ์คํธ ํญ๋ชฉ ์ปดํฌ๋ํธ โ memo๋ก ์ต์ ํ
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log(`TodoItem ${todo.id} ๋ ๋๋ง`)
return (
<li>
<input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} />
{todo.text}
<button onClick={() => onDelete(todo.id)}>์ญ์ </button>
</li>
)
})
function TodoList() {
const [todos, setTodos] = useState([...])
const [filter, setFilter] = useState('all')
// useCallback์ผ๋ก ๋ฉ๋ชจ์ด์ ์ด์
โ todos๊ฐ ๋ฐ๋์ด๋ ํจ์ ์ฐธ์กฐ ์ ์ง
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
}, []) // setTodos๋ ์์ ์ ์ด๋ฏ๋ก ์์กด์ฑ ๋ถํ์
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(t => t.id !== id))
}, [])
// filter๊ฐ ๋ฐ๋์ด๋ handleToggle, handleDelete๋ ์ฌ์์ฑ๋์ง ์์
// โ ๋ณ๊ฒฝ๋์ง ์์ TodoItem์ ๋ฆฌ๋ ๋๋ง๋์ง ์์
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
)
}7. useContext: ์ ์ญ ์ํ ๊ณต์
useContext๋ฅผ ์ฌ์ฉํ๋ฉด Props Drilling ์์ด ์ปดํฌ๋ํธ ํธ๋ฆฌ์ ์ด๋ ๊ณณ์์๋ ๋ฐ์ดํฐ๋ฅผ ๊ณต์ ํ ์ ์๋ค.
import { createContext, useContext, useState } from 'react'
// 1. Context ์์ฑ
const ThemeContext = createContext(null)
// 2. Provider ์ปดํฌ๋ํธ
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light')
}
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// 3. Context๋ฅผ ์ฌ์ฉํ๋ ์ปค์คํ
ํ
(ํธ์์ฑ + ์๋ฌ ์ฒ๋ฆฌ)
function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme์ ThemeProvider ์์์ ์ฌ์ฉํด์ผ ํฉ๋๋ค')
}
return context
}
// 4. ๊น์ด ์ค์ฒฉ๋ ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
function DeepComponent() {
const { theme, toggleTheme } = useTheme()
return (
<div
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff',
padding: '20px',
}}
>
<p>ํ์ฌ ํ
๋ง: {theme}</p>
<button onClick={toggleTheme}>ํ
๋ง ์ ํ</button>
</div>
)
}
// 5. ์ต์์์์ Provider๋ก ๊ฐ์ธ๊ธฐ
function App() {
return (
<ThemeProvider>
<Page />
</ThemeProvider>
)
}์ฌ๋ฌ Context ์กฐํฉํ๊ธฐ
// ์ธ์ฆ Context
const AuthContext = createContext(null)
function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
})
const userData = await response.json()
setUser(userData)
}
const logout = () => {
setUser(null)
localStorage.removeItem('token')
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
)
}
function useAuth() {
return useContext(AuthContext)
}
// ์ฌ์ฉ ์์
function Header() {
const { user, logout } = useAuth()
const { theme } = useTheme()
return (
<header style={{ backgroundColor: theme === 'dark' ? '#222' : '#f5f5f5' }}>
{user ? (
<>
<span>์๋
ํ์ธ์, {user.name}๋</span>
<button onClick={logout}>๋ก๊ทธ์์</button>
</>
) : (
<a href="/login">๋ก๊ทธ์ธ</a>
)}
</header>
)
}8. ์ปค์คํ ํ ๋ง๋ค๊ธฐ
์ปค์คํ
ํ
์ ์ฌ๋ฌ Hook์ ์กฐํฉํด์ ๋ก์ง์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋จ์๋ก ์ถ์ถํ ๊ฒ์ด๋ค. ์ด๋ฆ์ ๋ฐ๋์ use๋ก ์์ํด์ผ ํ๋ค.
useFetch: ๋ฐ์ดํฐ ํ์นญ ํ
import { useState, useEffect, useCallback } from 'react'
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const fetchData = useCallback(async () => {
if (!url) return
setLoading(true)
setError(null)
const controller = new AbortController()
try {
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`)
}
const json = await response.json()
setData(json)
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message)
}
} finally {
setLoading(false)
}
return () => controller.abort()
}, [url])
useEffect(() => {
fetchData()
}, [fetchData])
return { data, loading, error, refetch: fetchData }
}
// ์ฌ์ฉ ์์
function UserList() {
const { data: users, loading, error, refetch } = useFetch('/api/users')
if (loading) return <div>๋ก๋ฉ ์ค...</div>
if (error) return <div>์๋ฌ: {error} <button onClick={refetch}>์ฌ์๋</button></div>
if (!users) return null
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}useLocalStorage: localStorage ๋๊ธฐํ ํ
import { useState, useEffect } from 'react'
function useLocalStorage(key, initialValue) {
// ์ง์ฐ ์ด๊ธฐํ๋ก localStorage์์ ์ฝ๊ธฐ
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch (error) {
console.error(`useLocalStorage ์ด๊ธฐํ ์๋ฌ (key: ${key}):`, error)
return initialValue
}
})
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(`useLocalStorage ์ ์ฅ ์๋ฌ (key: ${key}):`, error)
}
}
const removeValue = () => {
try {
setStoredValue(initialValue)
localStorage.removeItem(key)
} catch (error) {
console.error(error)
}
}
return [storedValue, setValue, removeValue]
}
// ์ฌ์ฉ ์์
function Settings() {
const [theme, setTheme, removeTheme] = useLocalStorage('theme', 'light')
const [language, setLanguage] = useLocalStorage('language', 'ko')
return (
<div>
<label>
ํ
๋ง:
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">๋ผ์ดํธ</option>
<option value="dark">๋คํฌ</option>
</select>
</label>
<label>
์ธ์ด:
<select value={language} onChange={e => setLanguage(e.target.value)}>
<option value="ko">ํ๊ตญ์ด</option>
<option value="en">English</option>
</select>
</label>
<button onClick={removeTheme}>ํ
๋ง ์ด๊ธฐํ</button>
</div>
)
}useDebounce: ์ ๋ ฅ ์ง์ฐ ์ฒ๋ฆฌ ํ
import { useState, useEffect } from 'react'
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer) // value๊ฐ ๋ฐ๋๋ฉด ์ด์ ํ์ด๋จธ ์ทจ์
}, [value, delay])
return debouncedValue
}
// ์ฌ์ฉ ์์: ๊ฒ์์ฐฝ โ ์
๋ ฅ์ ๋ฉ์ถ๊ณ 300ms ํ์ ๊ฒ์ ์คํ
function SearchBox() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
const { data: results, loading } = useFetch(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
)
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="๊ฒ์์ด ์
๋ ฅ..."
/>
{loading && <p>๊ฒ์ ์ค...</p>}
{results && results.map(r => <p key={r.id}>{r.title}</p>)}
</div>
)
}9. Hooks ๊ท์น
Hooks๋ ๋ ๊ฐ์ง ์ค์ํ ๊ท์น์ ๋ฐ๋ผ์ผ ํ๋ค. ์ด ๊ท์น์ ์ด๊ธฐ๋ฉด ์์ธกํ ์ ์๋ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ๋ค.
๊ท์น 1: ์ต์์์์๋ง ํธ์ถ
Hook์ ๋ฐ๋ณต๋ฌธ, ์กฐ๊ฑด๋ฌธ, ์ค์ฒฉ ํจ์ ์์์ ํธ์ถํ ์ ์๋ค.
// ์๋ชป๋ ์ฝ๋
function BadComponent({ isLoggedIn }) {
if (isLoggedIn) {
const [user, setUser] = useState(null) // ์กฐ๊ฑด๋ฌธ ์์์ Hook ํธ์ถ โ ๊ธ์ง!
}
for (let i = 0; i < 3; i++) {
useEffect(() => {}) // ๋ฐ๋ณต๋ฌธ ์์์ Hook ํธ์ถ โ ๊ธ์ง!
}
}
// ์ฌ๋ฐ๋ฅธ ์ฝ๋
function GoodComponent({ isLoggedIn }) {
const [user, setUser] = useState(null) // ํญ์ ์ต์์์์
useEffect(() => {
if (isLoggedIn) { // ์กฐ๊ฑด๋ฌธ์ Hook ์์
fetchUser()
}
}, [isLoggedIn])
}React๋ Hook์ด ํธ์ถ๋๋ ์์๋ฅผ ํตํด ๊ฐ Hook์ State๋ฅผ ์ถ์ ํ๋ค. ์กฐ๊ฑด์ ๋ฐ๋ผ Hook์ด ํธ์ถ๋๋ฉด ์์๊ฐ ๋ฌ๋ผ์ ธ์ State๊ฐ ๋ค์์ธ๋ค.
๊ท์น 2: React ํจ์์์๋ง ํธ์ถ
Hook์ React ํจ์ํ ์ปดํฌ๋ํธ๋ ์ปค์คํ ํ ์์์๋ง ํธ์ถํ ์ ์๋ค.
// ์๋ชป๋ ์ฝ๋
function regularFunction() {
const [state, setState] = useState(0) // ์ผ๋ฐ ํจ์์์ Hook ํธ์ถ โ ๊ธ์ง!
}
// ์ฌ๋ฐ๋ฅธ ์ฝ๋
function MyComponent() {
const [state, setState] = useState(0) // ํจ์ํ ์ปดํฌ๋ํธ โ OK
}
function useCustomHook() {
const [state, setState] = useState(0) // ์ปค์คํ
ํ
โ OK
}eslint-plugin-react-hooks ํ๋ฌ๊ทธ์ธ์ ์ค์นํ๋ฉด ๊ท์น ์๋ฐ ์ ESLint๊ฐ ๊ฒฝ๊ณ ๋ฅผ ํ์ํด์ค๋ค.
10. ์์ฃผ ํ๋ ์ค์ ๋ชจ์
์ค์ 1: useEffect ์์กด์ฑ ๋๋ฝ
// ์๋ชป๋ ์ฝ๋: userId๊ฐ ๋ฐ๋์ด๋ effect๊ฐ ์ฌ์คํ๋์ง ์์
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
useEffect(() => {
fetchUser(userId).then(setUser)
}, []) // userId๋ฅผ ์์กด์ฑ์ ๋ฃ์ง ์์!
return <div>{user?.name}</div>
}
// ์ฌ๋ฐ๋ฅธ ์ฝ๋
useEffect(() => {
fetchUser(userId).then(setUser)
}, [userId]) // userId ํฌํจ์ค์ 2: ๋ถํ์ํ useEffect
// ์๋ชป๋ ์ฝ๋: ํ์ ๋ฐ์ดํฐ๋ฅผ ๊ตณ์ด State + useEffect๋ก ๊ด๋ฆฌ
function Order({ items }) {
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((sum, item) => sum + item.price, 0))
}, [items])
return <p>ํฉ๊ณ: {total}์</p>
}
// ์ฌ๋ฐ๋ฅธ ์ฝ๋: ๋ ๋๋ง ์ค์ ์ง์ ๊ณ์ฐ
function Order({ items }) {
const total = items.reduce((sum, item) => sum + item.price, 0)
return <p>ํฉ๊ณ: {total}์</p>
}์ค์ 3: ๊ฐ์ฒด/๋ฐฐ์ด์ ์์กด์ฑ์ ๋ฃ์ ๋
// ๋ฌธ์ : ๋งค ๋ ๋๋ง๋ง๋ค ์ ๊ฐ์ฒด๊ฐ ์์ฑ๋์ด ๋ฌดํ ๋ฃจํ
function Component({ userId }) {
const options = { method: 'GET' } // ๋งค๋ฒ ์ ๊ฐ์ฒด
useEffect(() => {
fetch(`/api/users/${userId}`, options)
}, [options]) // ํญ์ ์ ์ฐธ์กฐ์ด๋ฏ๋ก ๋งค๋ฒ ์คํ
// ํด๊ฒฐ ๋ฐฉ๋ฒ 1: useEffect ์์ผ๋ก ์ด๋
useEffect(() => {
const options = { method: 'GET' }
fetch(`/api/users/${userId}`, options)
}, [userId])
// ํด๊ฒฐ ๋ฐฉ๋ฒ 2: useMemo๋ก ๋ฉ๋ชจ์ด์ ์ด์
const options = useMemo(() => ({ method: 'GET' }), [])
}์ค์ 4: ๋ ๋๋ง ์ค ๋ถ์ ํจ๊ณผ
// ์๋ชป๋ ์ฝ๋
function BadComponent() {
const [data, setData] = useState(null)
// ๋ ๋๋ง ์ค์ ์ง์ fetch ํธ์ถ โ ๋งค ๋ ๋๋ง๋ง๋ค ์คํ๋จ!
fetch('/api/data').then(r => r.json()).then(setData)
return <div>{data?.name}</div>
}
// ์ฌ๋ฐ๋ฅธ ์ฝ๋
function GoodComponent() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData)
}, [])
return <div>{data?.name}</div>
}11. Hook ํ๋์ ๋น๊ต
| Hook | ์ฉ๋ | ๋ฆฌ๋ ๋๋ง ํธ๋ฆฌ๊ฑฐ | ์ฃผ์ ์ฌ์ฉ ์ฌ๋ก |
|---|---|---|---|
useState |
์ปดํฌ๋ํธ ์ํ ๊ด๋ฆฌ | ๊ฐ ๋ณ๊ฒฝ ์ | ํผ, ์นด์ดํฐ, ํ ๊ธ |
useEffect |
์ฌ์ด๋ ์ดํํธ ์ฒ๋ฆฌ | ์์ | API ํธ์ถ, ์ด๋ฒคํธ ๋ฆฌ์ค๋ |
useRef |
DOM ์ฐธ์กฐ, ๊ฐ ์ ์ง | ์์ | ํฌ์ปค์ค, ํ์ด๋จธ ID ์ ์ฅ |
useMemo |
๊ณ์ฐ ๊ฒฐ๊ณผ ์บ์ฑ | ์์ | ๋ฌด๊ฑฐ์ด ์ฐ์ฐ, ํ์ ๋ฐ์ดํฐ |
useCallback |
ํจ์ ๋ฉ๋ชจ์ด์ ์ด์ | ์์ | ์์ ์ปดํฌ๋ํธ์ ์ฝ๋ฐฑ ์ ๋ฌ |
useContext |
Context ๊ฐ ์ฝ๊ธฐ | Context ๋ณ๊ฒฝ ์ | ์ ์ญ ์ํ, ํ ๋ง, ์ธ์ฆ |
๋ง์น๋ฉฐ
React Hooks๋ ํจ์ํ ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ ฅํ๊ฒ ๋ง๋ค์ด์ฃผ๋ ํต์ฌ ๊ธฐ๋ฅ์ด๋ค. ํต์ฌ ์์น์ ์ ๋ฆฌํ์๋ฉด:
- useState: ๊ฐ์ด ๋ณํ ๋๋ง๋ค ํ๋ฉด์ ๋ค์ ๊ทธ๋ ค์ผ ํ๋ค๋ฉด State๋ก ๊ด๋ฆฌ
- useEffect: ๋ ๋๋ง ์ดํ์ ์คํํด์ผ ํ๋ ์์ (API ํธ์ถ, ๊ตฌ๋ ๋ฑ)์ ์ฌ์ฉ
- useRef: ๋ ๋๋ง๊ณผ ๋ฌด๊ดํ๊ฒ ๊ฐ์ ์ ์งํ๊ฑฐ๋ DOM์ ์ง์ ์ ๊ทผํ ๋
- useMemo/useCallback: ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ์ค์ ๋ก ํ์ธ๋์์ ๋๋ง ์ฌ์ฉ
- useContext: Props Drilling์ด ๊น์ด์ง ๋ ์ ์ญ ์ํ ๊ณต์
์ปค์คํ ํ ์ ์ ๋ง๋ค๋ฉด ๋ก์ง์ ๊น๋ํ๊ฒ ์ฌ์ฌ์ฉํ ์ ์๋ค. ์ค๋ณต๋๋ ๋ก์ง์ด ๋ณด์ธ๋ค๋ฉด ์ปค์คํ ํ ์ผ๋ก ์ถ์ถํ๋ ์ต๊ด์ ๋ค์ด์.
๊ด๋ จ ๊ธ
- ๐ React ์ ๋ฌธ: SPA, ์ปดํฌ๋ํธ, JSX ํต์ฌ ๊ฐ๋ โ React ๊ธฐ์ด 1ํธ
- ๐ React Props์ State ์์ ์ ๋ณต โ React ๊ธฐ์ด 2ํธ
- โก JavaScript ๋น๋๊ธฐ ์ฒ๋ฆฌ ์์ ๊ฐ์ด๋ โ useEffect์ ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๊ธฐ์ด
