React ๊ธฐ์ด ์ ๋ฆฌ โ Props์ State ์์ ์ ๋ณต
React๋ฅผ ์ฒ์ ๋ฐฐ์ธ ๋ ๊ฐ์ฅ ๋จผ์ ๋ง์ฃผ์น๋ ๋ ๊ฐ๋ ์ด ๋ฐ๋ก Props์ State๋ค. ์ด ๋ ๊ฐ์ง๋ฅผ ์ ๋๋ก ์ดํดํด์ผ React ์ปดํฌ๋ํธ๋ฅผ ์ฌ๋ฐ๋ฅด๊ฒ ์ค๊ณํ ์ ์๋ค. ์ด ๊ธ์์๋ ๊ธฐ์ด๋ถํฐ ์ฌํ๊น์ง, ๊ทธ๋ฆฌ๊ณ ์ค์ ์์ ์์ฃผ ํ๋ ์ค์๊น์ง ๋ชจ๋ ๋ค๋ฃฌ๋ค.
1. Props๋ ๋ฌด์์ธ๊ฐ?
Props๋ Properties์ ์ค์๋ง๋ก, ๋ถ๋ชจ ์ปดํฌ๋ํธ๊ฐ ์์ ์ปดํฌ๋ํธ์๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ ๋ ์ฌ์ฉํ๋ ๋ฉ์ปค๋์ฆ์ด๋ค. ํจ์์ ์ธ์(argument)์ ๊ฐ์ ์ญํ ์ ํ๋ค.
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
// ์์ ์ปดํฌ๋ํธ
const Welcome = (props) => {
return <h1>Hello, {props.name}!</h1>
}
// ๋ถ๋ชจ ์ปดํฌ๋ํธ
const App = () => {
return (
<div>
<Welcome name="Amy" />
<Welcome name="Andy" />
<Welcome name="Sapiens" />
</div>
)
}์ ์์ ์์ name์ด๋ผ๋ props๋ฅผ ์์ ์ปดํฌ๋ํธ Welcome์ ์ ๋ฌํ๊ณ ์๋ค. JSX์์ HTML ์์ฑ์ฒ๋ผ ์์ฑํ๋ฉด ์์ ์ปดํฌ๋ํธ์ props ๊ฐ์ฒด๋ฅผ ํตํด ์ ๊ทผํ ์ ์๋ค.
๊ตฌ์กฐ ๋ถํด ํ ๋น์ผ๋ก ๊น๋ํ๊ฒ ์ฐ๊ธฐ
๋งค๋ฒ props.name์ฒ๋ผ ์ฐ๋ ๋์ , ES6 ๊ตฌ์กฐ ๋ถํด ํ ๋น์ ์ฌ์ฉํ๋ฉด ๋ ๊น๋ํ๋ค.
// ๋ฐฉ๋ฒ 1: ํ๋ผ๋ฏธํฐ์์ ๋ฐ๋ก ๊ตฌ์กฐ๋ถํด
const Welcome = ({ name, age, city }) => {
return (
<div>
<h1>Hello, {name}!</h1>
<p>Age: {age}</p>
<p>City: {city}</p>
</div>
)
}
// ๋ฐฉ๋ฒ 2: ํจ์ ๋ด๋ถ์์ ๊ตฌ์กฐ๋ถํด
const Welcome = (props) => {
const { name, age, city } = props
return (
<div>
<h1>Hello, {name}!</h1>
<p>Age: {age}</p>
<p>City: {city}</p>
</div>
)
}2. Props ์ฌํ: defaultProps๋ก ๊ธฐ๋ณธ๊ฐ ์ค์ ํ๊ธฐ
๋ถ๋ชจ ์ปดํฌ๋ํธ๊ฐ ํน์ props๋ฅผ ์ ๋ฌํ์ง ์์์ ๋ ๊ธฐ๋ณธ๊ฐ์ ์ง์ ํ ์ ์๋ค. ์ด๋ฅผ defaultProps๋ผ๊ณ ํ๋ค.
const Button = ({ label, color, size }) => {
return (
<button
style={{
backgroundColor: color,
fontSize: size === 'large' ? '18px' : '14px',
padding: '8px 16px',
}}
>
{label}
</button>
)
}
// defaultProps ์ค์
Button.defaultProps = {
label: 'ํด๋ฆญ',
color: '#4CAF50',
size: 'medium',
}
// ์ฌ์ฉ: label๋ง ์ ๋ฌํด๋ ๋๋จธ์ง๋ ๊ธฐ๋ณธ๊ฐ์ผ๋ก
const App = () => {
return (
<div>
<Button label="์ ์ฅ" color="#2196F3" />
<Button label="์ญ์ " color="#f44336" size="large" />
<Button /> {/* ๋ชจ๋ ๊ธฐ๋ณธ๊ฐ ์ฌ์ฉ */}
</div>
)
}์ฐธ๊ณ : React ํจ์ํ ์ปดํฌ๋ํธ์์๋
defaultProps๋์ ES6 ๊ธฐ๋ณธ๊ฐ ๋ฌธ๋ฒ์ ๋ ๋ง์ด ์ฌ์ฉํ๋ค.
// defaultProps ๋์ ๊ธฐ๋ณธ๊ฐ ํ๋ผ๋ฏธํฐ ์ฌ์ฉ (๋ ํ๋์ ์ธ ๋ฐฉ์)
const Button = ({ label = 'ํด๋ฆญ', color = '#4CAF50', size = 'medium' }) => {
return (
<button style={{ backgroundColor: color }}>
{label}
</button>
)
}3. PropTypes๋ก ํ์ ๊ฒ์ฌํ๊ธฐ
JavaScript๋ ๋์ ํ์
์ธ์ด๋ผ ์๋ชป๋ ํ์
์ props๋ฅผ ์ ๋ฌํด๋ ๋ฐ๋ก ์๋ฌ๊ฐ ๋์ง ์๋๋ค. prop-types ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ๋ฐ ํ๊ฒฝ์์ ๊ฒฝ๊ณ ๋ฅผ ๋ฐ์ ์ ์๋ค.
npm install prop-typesimport PropTypes from 'prop-types'
const UserCard = ({ name, age, isAdmin, hobbies, onClick }) => {
return (
<div className="user-card">
<h2>{name} {isAdmin && '(๊ด๋ฆฌ์)'}</h2>
<p>๋์ด: {age}์ธ</p>
<ul>
{hobbies.map((hobby, index) => (
<li key={index}>{hobby}</li>
))}
</ul>
<button onClick={onClick}>ํ๋กํ ๋ณด๊ธฐ</button>
</div>
)
}
// PropTypes ์ ์
UserCard.propTypes = {
name: PropTypes.string.isRequired, // ๋ฌธ์์ด, ํ์
age: PropTypes.number.isRequired, // ์ซ์, ํ์
isAdmin: PropTypes.bool, // ๋ถ๋ฆฌ์ธ, ์ ํ
hobbies: PropTypes.arrayOf(PropTypes.string), // ๋ฌธ์์ด ๋ฐฐ์ด
onClick: PropTypes.func, // ํจ์
}
UserCard.defaultProps = {
isAdmin: false,
hobbies: [],
onClick: () => {},
}PropTypes๊ฐ ์ ๊ณตํ๋ ์ฃผ์ ํ์
๊ฒ์ฌ๊ธฐ:
| ํ์ ๊ฒ์ฌ๊ธฐ | ์ค๋ช |
|---|---|
PropTypes.string |
๋ฌธ์์ด |
PropTypes.number |
์ซ์ |
PropTypes.bool |
๋ถ๋ฆฌ์ธ |
PropTypes.array |
๋ฐฐ์ด |
PropTypes.object |
๊ฐ์ฒด |
PropTypes.func |
ํจ์ |
PropTypes.node |
๋ ๋๋ง ๊ฐ๋ฅํ ๋ชจ๋ ๊ฒ |
PropTypes.element |
React ์๋ฆฌ๋จผํธ |
PropTypes.arrayOf(type) |
ํน์ ํ์ ์ ๋ฐฐ์ด |
PropTypes.shape({}) |
ํน์ ๊ตฌ์กฐ์ ๊ฐ์ฒด |
.isRequired |
ํ์ props ์ง์ |
TypeScript๋ฅผ ์ฌ์ฉํ๋ค๋ฉด? TypeScript๋ฅผ ์ธ ๊ฒฝ์ฐ PropTypes ์์ด๋ ํ์ ์์ ์ฑ์ ํ๋ณดํ ์ ์๋ค. ์ต์ ํ๋ก์ ํธ์์๋ TypeScript + React๋ฅผ ์กฐํฉํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค.
4. children prop: ์ปดํฌ๋ํธ ์์ ์ปดํฌ๋ํธ ๋ฃ๊ธฐ
React์๋ ํน๋ณํ props๊ฐ ์๋๋ฐ, ๋ฐ๋ก children์ด๋ค. JSX ํ๊ทธ ์ฌ์ด์ ๋ฃ์ ๋ด์ฉ์ด children์ผ๋ก ์ ๋ฌ๋๋ค.
// children์ ๋ฐ๋ ์ปดํฌ๋ํธ (๋ํผ ์ปดํฌ๋ํธ)
const Card = ({ title, children, className }) => {
return (
<div className={`card ${className}`}>
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">
{children} {/* ์ฌ๊ธฐ์ ์์ ์์๋ค์ด ๋ ๋๋ง๋จ */}
</div>
</div>
)
}
// ์ฌ์ฉ
const App = () => {
return (
<div>
<Card title="์ฌ์ฉ์ ์ ๋ณด" className="user-card">
<p>์ด๋ฆ: Amy</p>
<p>์ด๋ฉ์ผ: amy@example.com</p>
<button>์์ </button>
</Card>
<Card title="๊ณต์ง์ฌํญ">
<ul>
<li>์์คํ
์ ๊ฒ ์๋ด</li>
<li>์ ๊ธฐ๋ฅ ์
๋ฐ์ดํธ</li>
</ul>
</Card>
</div>
)
}children์ ํ์ฉํ๋ฉด ๋ ์ด์์ ์ปดํฌ๋ํธ, ๋ชจ๋ฌ, ์นด๋ ๊ฐ์ ์ปจํ
์ด๋ ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ค ๋ ๋งค์ฐ ์ ์ฉํ๋ค.
5. Spread Props: props ํ๊บผ๋ฒ์ ์ ๋ฌํ๊ธฐ
์ฌ๋ฌ props๋ฅผ ํ ๋ฒ์ ์ ๋ฌํ๊ณ ์ถ์ ๋ ์คํ๋ ๋ ์ฐ์ฐ์๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
const Input = ({ label, id, ...rest }) => {
// label, id๋ ๋ฐ๋ก ์ฌ์ฉํ๊ณ , ๋๋จธ์ง ์์ฑ๋ค์ input ์๋ฆฌ๋จผํธ์ ์ ๋ฌ
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} {...rest} />
</div>
)
}
const App = () => {
const inputProps = {
type: 'email',
placeholder: '์ด๋ฉ์ผ์ ์
๋ ฅํ์ธ์',
required: true,
autoComplete: 'email',
}
return (
<Input
label="์ด๋ฉ์ผ"
id="email"
{...inputProps} // ์คํ๋ ๋๋ก ํ๊บผ๋ฒ์ ์ ๋ฌ
/>
)
}๋ค๋ง ์คํ๋ ๋ props๋ ๋จ์ฉํ๋ฉด ์ด๋ค props๊ฐ ์ ๋ฌ๋๋์ง ์ถ์ ํ๊ธฐ ์ด๋ ค์์ง๋ฏ๋ก ์ ์คํ๊ฒ ์ฌ์ฉํด์ผ ํ๋ค.
6. Props Drilling ๋ฌธ์
์ปดํฌ๋ํธ๊ฐ ๊น์ด ์ค์ฒฉ๋ ์๋ก ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๊ธฐ ์ํด ์ค๊ฐ ์ปดํฌ๋ํธ๋ค์ด ์ฌ์ฉํ์ง๋ ์๋ props๋ฅผ ๊ณ์ ๋ด๋ ค์ค์ผ ํ๋ ๋ฌธ์ ๊ฐ ์๊ธด๋ค. ์ด๋ฅผ Props Drilling์ด๋ผ๊ณ ํ๋ค.
// Props Drilling ์์: theme์ ๊ณ์ ๋ด๋ ค์ค์ผ ํจ
const App = () => {
const theme = 'dark'
return <Page theme={theme} />
}
const Page = ({ theme }) => {
return <Sidebar theme={theme} />
}
const Sidebar = ({ theme }) => {
return <Menu theme={theme} />
}
const Menu = ({ theme }) => {
return <MenuItem theme={theme} />
}
const MenuItem = ({ theme }) => {
// ๋๋์ด ์ค์ ๋ก theme์ ์ฌ์ฉํ๋ ์ปดํฌ๋ํธ
return (
<li style={{ color: theme === 'dark' ? 'white' : 'black' }}>
๋ฉ๋ด ํญ๋ชฉ
</li>
)
}Page, Sidebar, Menu๋ theme์ ์ง์ ์ฌ์ฉํ์ง ์์ง๋ง ์ ๋ฌ๋ง ํ๋ค. ์ปดํฌ๋ํธ๊ฐ 5๋จ๊ณ, 10๋จ๊ณ๋ก ๊น์ด์ง๋ฉด ๊ด๋ฆฌ๊ฐ ๋งค์ฐ ์ด๋ ค์์ง๋ค.
ํด๊ฒฐ ๋ฐฉ๋ฒ:
- Context API: React ๋ด์ฅ ์ ์ญ ์ํ ๊ณต์ (๋ค์ ๊ธ์์ ๋ค๋ฃธ)
- ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ: Redux, Zustand, Jotai ๋ฑ
- ์ปดํฌ๋ํธ ํฉ์ฑ(Composition): children์ด๋ render props ํจํด ํ์ฉ
7. State๋ ๋ฌด์์ธ๊ฐ?
State๋ ์ปดํฌ๋ํธ ๋ด๋ถ์์ ๊ด๋ฆฌ๋๋ ๋์ ๋ฐ์ดํฐ๋ค. ์ฌ์ฉ์ ์ํธ์์ฉ, API ์๋ต, ์๊ฐ ๊ฒฝ๊ณผ ๋ฑ์ผ๋ก ์ธํด ๋ณํ ์ ์๋ ๊ฐ์ State๋ก ๊ด๋ฆฌํ๋ค.
Props๊ฐ "์ธ๋ถ์์ ์ฃผ์ด์ง๋ ๊ฐ"์ด๋ผ๋ฉด, State๋ "์ปดํฌ๋ํธ ์ค์ค๋ก ๊ด๋ฆฌํ๋ ๊ฐ"์ด๋ค.
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>ํ์ฌ ์นด์ดํธ: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
<button onClick={() => setCount(count - 1)}>-1</button>
<button onClick={() => setCount(0)}>์ด๊ธฐํ</button>
</div>
)
}useState๋ ๋ฐฐ์ด์ ๋ฐํํ๋๋ฐ, ์ฒซ ๋ฒ์งธ ์์๋ ํ์ฌ State ๊ฐ, ๋ ๋ฒ์งธ ์์๋ State๋ฅผ ๋ณ๊ฒฝํ๋ ํจ์๋ค.
8. State ์ฌํ: ๋น๋๊ธฐ ์ ๋ฐ์ดํธ ์ฃผ์์
State ์ ๋ฐ์ดํธ๋ ์ฆ๊ฐ์ ์ผ๋ก ๋ฐ์๋์ง ์๋๋ค. React๋ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด ์ฌ๋ฌ State ์ ๋ฐ์ดํธ๋ฅผ ๋ฌถ์ด์ ํ ๋ฒ์ ์ฒ๋ฆฌ(๋ฐฐ์น ์ฒ๋ฆฌ)ํ๋ค.
function Counter() {
const [count, setCount] = useState(0)
const handleBadClick = () => {
// ์ด๋ ๊ฒ ํ๋ฉด 3์ด ์๋๋ผ 1๋ง ์ฆ๊ฐํ ์ ์๋ค!
setCount(count + 1)
setCount(count + 1)
setCount(count + 1)
// ์ธ ๋ฒ ๋ชจ๋ ๊ฐ์ 'count' ๊ฐ์ ์ฐธ์กฐํ๊ธฐ ๋๋ฌธ
}
const handleGoodClick = () => {
// ํจ์ํ ์
๋ฐ์ดํธ๋ฅผ ์ฌ์ฉํ๋ฉด ์ต์ state๋ฅผ ๋ณด์ฅ
setCount(prev => prev + 1)
setCount(prev => prev + 1)
setCount(prev => prev + 1)
// ์ด๋ ๊ฒ ํ๋ฉด ์ ํํ 3์ด ์ฆ๊ฐ
}
return (
<div>
<p>์นด์ดํธ: {count}</p>
<button onClick={handleBadClick}>์๋ชป๋ ๋ฐฉ๋ฒ (+3 ์๋)</button>
<button onClick={handleGoodClick}>์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ (+3)</button>
</div>
)
}ํต์ฌ ์์น: ์ด์ State๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ State๋ฅผ ๊ณ์ฐํ ๋๋ ๋ฐ๋์ ํจ์ํ ์
๋ฐ์ดํธ (prev => prev + 1)๋ฅผ ์ฌ์ฉํ๋ค.
9. ์ฌ๋ฌ State ๊ด๋ฆฌํ๊ธฐ
ํ๋์ ์ปดํฌ๋ํธ์์ ์ฌ๋ฌ State๋ฅผ ๊ด๋ฆฌํ ๋๋ ๋ ๊ฐ์ง ์ ๊ทผ๋ฒ์ด ์๋ค.
๋ฐฉ๋ฒ 1: ์ฌ๋ฌ useState ์ฌ์ฉ (๋จ์ํ ๊ฐ์ ์ ํฉ)
function UserForm() {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [age, setAge] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setIsSubmitting(true)
// API ํธ์ถ...
setIsSubmitting(false)
}
return (
<form onSubmit={handleSubmit}>
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="์ด๋ฆ" />
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="์ด๋ฉ์ผ" />
<input value={age} onChange={(e) => setAge(e.target.value)} placeholder="๋์ด" />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '์ ์ก ์ค...' : '์ ์ถ'}
</button>
</form>
)
}๋ฐฉ๋ฒ 2: ๊ฐ์ฒด State ์ฌ์ฉ (๊ด๋ จ ๋ฐ์ดํฐ ๋ฌถ๊ธฐ)
function UserForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
age: '',
})
const [isSubmitting, setIsSubmitting] = useState(false)
// ๋ฒ์ฉ ํธ๋ค๋ฌ: input name ์์ฑ์ ์ด์ฉ
const handleChange = (e) => {
const { name, value } = e.target
setFormData(prev => ({
...prev, // ๊ธฐ์กด ๋ฐ์ดํฐ ๋ณต์ฌ (๋ถ๋ณ์ฑ ์ ์ง!)
[name]: value // ํด๋น ํ๋๋ง ์
๋ฐ์ดํธ
}))
}
return (
<form>
<input name="name" value={formData.name} onChange={handleChange} placeholder="์ด๋ฆ" />
<input name="email" value={formData.email} onChange={handleChange} placeholder="์ด๋ฉ์ผ" />
<input name="age" value={formData.age} onChange={handleChange} placeholder="๋์ด" />
</form>
)
}10. ๋ฐฐ์ด State ๋ค๋ฃจ๊ธฐ
๋ฐฐ์ด์ State๋ก ๊ด๋ฆฌํ ๋๋ ๋ถ๋ณ์ฑ์ ์ง์ผ์ผ ํ๋ค. ๋ฐฐ์ด์ ์ง์ ๋ณ๊ฒฝํ๋ ๋ฉ์๋(push, pop, splice ๋ฑ)๋ ์ฌ์ฉํ์ง ๋ง๊ณ , ์๋ก์ด ๋ฐฐ์ด์ ๋ฐํํ๋ ๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'React ๊ณต๋ถํ๊ธฐ', done: false },
{ id: 2, text: '์ด๋ํ๊ธฐ', done: true },
])
const [inputText, setInputText] = useState('')
// ํญ๋ชฉ ์ถ๊ฐ
const addTodo = () => {
if (!inputText.trim()) return
const newTodo = {
id: Date.now(),
text: inputText,
done: false,
}
setTodos(prev => [...prev, newTodo]) // ์ ๋ฐฐ์ด ์์ฑ
setInputText('')
}
// ์๋ฃ ํ ๊ธ
const toggleTodo = (id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
)
}
// ํญ๋ชฉ ์ญ์
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}
return (
<div>
<div>
<input
value={inputText}
onChange={(e) => setInputText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="ํ ์ผ ์
๋ ฅ..."
/>
<button onClick={addTodo}>์ถ๊ฐ</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id)}
/>
{todo.text}
<button onClick={() => deleteTodo(todo.id)}>์ญ์ </button>
</li>
))}
</ul>
<p>์ด {todos.length}๊ฐ / ์๋ฃ {todos.filter(t => t.done).length}๊ฐ</p>
</div>
)
}๋ฐฐ์ด ์กฐ์ ์ ๋ถ๋ณ์ฑ์ ์งํค๋ ํจํด ์ ๋ฆฌ:
| ์์ | ์๋ชป๋ ๋ฐฉ๋ฒ (์ง์ ๋ณ๊ฒฝ) | ์ฌ๋ฐ๋ฅธ ๋ฐฉ๋ฒ (์ ๋ฐฐ์ด) |
|---|---|---|
| ์ถ๊ฐ | arr.push(item) |
[...arr, item] |
| ์์ ์ถ๊ฐ | arr.unshift(item) |
[item, ...arr] |
| ์ญ์ | arr.splice(idx, 1) |
arr.filter((_, i) => i !== idx) |
| ์์ | arr[idx] = newItem |
arr.map((item, i) => i === idx ? newItem : item) |
| ์ ๋ ฌ | arr.sort() |
[...arr].sort() |
11. ๋ถ๋ณ์ฑ(Immutability)์ด๋? ์ ์ค์ํ๊ฐ?
๋ถ๋ณ์ฑ์ "ํ ๋ฒ ๋ง๋ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์์ ํ์ง ์๋๋ค"๋ ์์น์ด๋ค. React์์ ๋ถ๋ณ์ฑ์ด ์ค์ํ ์ด์ ๋ React๊ฐ State ๋ณ๊ฒฝ์ ๊ฐ์งํ๋ ๋ฐฉ์ ๋๋ฌธ์ด๋ค.
React์ ๋ณ๊ฒฝ ๊ฐ์ง ๋ฐฉ์
React๋ State๊ฐ ๋ณ๊ฒฝ๋์๋์ง ํ์ธํ๊ธฐ ์ํด **์์ ๋น๊ต(Shallow Comparison)**๋ฅผ ์ฌ์ฉํ๋ค. ์ฆ, ๊ฐ์ฒด์ ๋ด์ฉ์ด ๋ฐ๋์๋์ง ๊น์ด ๋น๊ตํ๋ ๊ฒ์ด ์๋๋ผ, ์ฐธ์กฐ(Reference)๊ฐ ๋ฐ๋์๋์ง๋ฅผ ํ์ธํ๋ค.
// ์๋ชป๋ ์: ์ฐธ์กฐ๊ฐ ๋ฐ๋์ง ์์ React๊ฐ ๋ณ๊ฒฝ์ ๊ฐ์ง ๋ชปํจ
const [user, setUser] = useState({ name: 'Amy', age: 30 })
const badUpdate = () => {
user.age = 31 // ์ง์ ์์
setUser(user) // ๊ฐ์ ์ฐธ์กฐ์ด๋ฏ๋ก React๊ฐ ๋ณ๊ฒฝ์ ๊ฐ์ง ๋ชปํจ!
}
// ์ฌ๋ฐ๋ฅธ ์: ์ ๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ ์ฐธ์กฐ ๋ณ๊ฒฝ
const goodUpdate = () => {
setUser(prev => ({
...prev, // ๊ธฐ์กด ๊ฐ์ฒด์ ์์ฑ์ ๋ณต์ฌ
age: 31 // ๋ณ๊ฒฝํ ์์ฑ๋ง ๋ฎ์ด์
}))
}์ค์ฒฉ ๊ฐ์ฒด์์์ ๋ถ๋ณ์ฑ
์ค์ฒฉ ๊ฐ์ฒด์์๋ ์คํ๋ ๋ ์ฐ์ฐ์๊ฐ ์์ ๋ณต์ฌ๋ฅผ ํ๊ธฐ ๋๋ฌธ์ ์ฃผ์๊ฐ ํ์ํ๋ค.
const [state, setState] = useState({
user: {
name: 'Amy',
address: {
city: '์์ธ',
district: '๊ฐ๋จ๊ตฌ',
}
}
})
// ์ค์ฒฉ๋ ๊ฐ์ฒด๋ฅผ ์์ ํ๊ฒ ์
๋ฐ์ดํธ
const updateCity = (newCity) => {
setState(prev => ({
...prev,
user: {
...prev.user,
address: {
...prev.user.address,
city: newCity,
}
}
}))
}๊น์ด ์ค์ฒฉ๋ ๊ฐ์ฒด๋ฅผ ๋ค๋ฃฐ์๋ก ์ฝ๋๊ฐ ๋ณต์กํด์ง๋ค. ์ด๋ฅผ ํด๊ฒฐํด์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋ค.
Immer ๋ผ์ด๋ธ๋ฌ๋ฆฌ: ๋ถ๋ณ์ฑ์ ์ฝ๊ฒ
immer๋ ๋ถ๋ณ์ฑ์ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค๋ค. ๋ง์น ์ง์ ์์ ํ๋ ๊ฒ์ฒ๋ผ ์ฝ๋๋ฅผ ์์ฑํด๋, ๋ด๋ถ์ ์ผ๋ก๋ ์ ๊ฐ์ฒด๋ฅผ ์์ฑํด์ค๋ค.
npm install immerimport { useState } from 'react'
import { produce } from 'immer'
const [state, setState] = useState({
user: {
name: 'Amy',
address: {
city: '์์ธ',
district: '๊ฐ๋จ๊ตฌ',
}
},
todos: [
{ id: 1, text: '๊ณต๋ถ', done: false }
]
})
// immer ์ฌ์ฉ: ์ง์ ์์ ํ๋ ๊ฒ์ฒ๋ผ ์ฝ๋ ์์ฑ
const updateCity = (newCity) => {
setState(produce(draft => {
draft.user.address.city = newCity // ์ง์ ์์ ์ฒ๋ผ ๋ณด์ด์ง๋ง ์์
}))
}
const toggleTodo = (id) => {
setState(produce(draft => {
const todo = draft.todos.find(t => t.id === id)
if (todo) todo.done = !todo.done // ์ง์ ์์
}))
}immer๋ ํนํ ์ค์ฒฉ์ด ๊น์ State๋ฅผ ๋ค๋ฃฐ ๋ ์ฝ๋ ๊ฐ๋
์ฑ์ ํฌ๊ฒ ํฅ์์ํจ๋ค.
12. ๋ฆฌ๋ ๋๋ง: ์ธ์ ์ปดํฌ๋ํธ๊ฐ ๋ค์ ๋ ๋๋ง๋๋?
React ์ปดํฌ๋ํธ๋ ๋ค์ ์ธ ๊ฐ์ง ์ํฉ์์ ๋ฆฌ๋ ๋๋ง๋๋ค:
- ์์ ์ State๊ฐ ๋ณ๊ฒฝ๋ ๋
- ๋ถ๋ชจ๋ก๋ถํฐ ๋ฐ๋ Props๊ฐ ๋ณ๊ฒฝ๋ ๋
- ๋ถ๋ชจ ์ปดํฌ๋ํธ๊ฐ ๋ฆฌ๋ ๋๋ง๋ ๋ (props๊ฐ ๋ฐ๋์ง ์์๋!)
const Parent = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('Amy')
console.log('Parent ๋ ๋๋ง')
return (
<div>
<button onClick={() => setCount(c => c + 1)}>์นด์ดํธ ์ฆ๊ฐ</button>
<Child name={name} /> {/* count๊ฐ ๋ฐ๋์ด๋ Child๋ ๋ฆฌ๋ ๋๋ง๋จ! */}
</div>
)
}
const Child = ({ name }) => {
console.log('Child ๋ ๋๋ง')
return <p>์ด๋ฆ: {name}</p>
}๋ฒํผ์ ํด๋ฆญํ๋ฉด count๊ฐ ๋ณํด์ Parent๊ฐ ๋ฆฌ๋ ๋๋ง๋๊ณ , Child๋ ๋ฐ๋ผ์ ๋ฆฌ๋ ๋๋ง๋๋ค. name์ ๋ฐ๋์ง ์์๋๋ฐ๋ ๋ง์ด๋ค.
13. React.memo๋ก ๋ถํ์ํ ๋ ๋๋ง ๋ฐฉ์ง
React.memo๋ ์ปดํฌ๋ํธ๋ฅผ ๋ฉ๋ชจ์ด์ ์ด์
ํ๋ ๊ณ ์ฐจ ์ปดํฌ๋ํธ(HOC)๋ค. props๊ฐ ๋ณ๊ฒฝ๋์ง ์์ผ๋ฉด ๋ฆฌ๋ ๋๋ง์ ๊ฑด๋๋ด๋ค.
import { useState, memo } from 'react'
const Parent = () => {
const [count, setCount] = useState(0)
const [name, setName] = useState('Amy')
return (
<div>
<button onClick={() => setCount(c => c + 1)}>์นด์ดํธ: {count}</button>
<button onClick={() => setName('Andy')}>์ด๋ฆ ๋ณ๊ฒฝ</button>
<MemoizedChild name={name} />
</div>
)
}
// React.memo๋ก ๊ฐ์ธ๊ธฐ
const MemoizedChild = memo(({ name }) => {
console.log('Child ๋ ๋๋ง')
return <p>์ด๋ฆ: {name}</p>
})
// ๋๋ ์ปดํฌ๋ํธ๋ฅผ ์ ์ธํ ๋ ๋ฐ๋ก ๊ฐ์ธ๊ธฐ
const Child = memo(({ name }) => {
return <p>์ด๋ฆ: {name}</p>
})์ด์ count๊ฐ ๋ณํด๋ Child๋ ๋ฆฌ๋ ๋๋ง๋์ง ์๋๋ค. name์ด ๋ฐ๋ ๋๋ง ๋ฆฌ๋ ๋๋ง๋๋ค.
React.memo์ ์ปค์คํ ๋น๊ต ํจ์
๊ธฐ๋ณธ์ ์ผ๋ก React.memo๋ ์์ ๋น๊ต๋ฅผ ์ฌ์ฉํ์ง๋ง, ๋ ๋ฒ์งธ ์ธ์๋ก ์ปค์คํ
๋น๊ต ํจ์๋ฅผ ์ ๋ฌํ ์ ์๋ค.
const MyComponent = memo(
({ data }) => {
return <div>{data.value}</div>
},
(prevProps, nextProps) => {
// true๋ฅผ ๋ฐํํ๋ฉด ๋ฆฌ๋ ๋๋ง ์ ํจ
// false๋ฅผ ๋ฐํํ๋ฉด ๋ฆฌ๋ ๋๋ง ํจ
return prevProps.data.id === nextProps.data.id
}
)์ฃผ์:
React.memo๋ฅผ ๋จ์ฉํ๋ฉด ์คํ๋ ค ์ฑ๋ฅ์ด ๋๋น ์ง ์ ์๋ค. ๋น๊ต ์์ฒด์๋ ๋น์ฉ์ด ๋ค๊ธฐ ๋๋ฌธ์ด๋ค. ์ค์ ๋ก ์ฑ๋ฅ ๋ฌธ์ ๊ฐ ์์ ๋๋ง ์ฌ์ฉํ๋ค.
14. ์ค์ ์์ : TodoList ์์ฑ ๊ตฌํ
์ง๊ธ๊น์ง ๋ฐฐ์ด Props, State, ๋ถ๋ณ์ฑ, React.memo๋ฅผ ๋ชจ๋ ํ์ฉํ ์์ฑ๋ TodoList ์์ ๋ค.
import { useState, memo, useCallback } from 'react'
// ํ ์ผ ํญ๋ชฉ ์ปดํฌ๋ํธ - React.memo๋ก ์ต์ ํ
const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log(`TodoItem ๋ ๋๋ง: ${todo.id}`)
return (
<li
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#aaa' : '#333',
padding: '8px 0',
}}
>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span style={{ flex: 1 }}>{todo.text}</span>
<span style={{ fontSize: '12px', color: '#999' }}>
{new Date(todo.createdAt).toLocaleDateString()}
</span>
<button
onClick={() => onDelete(todo.id)}
style={{ color: '#f44336', background: 'none', border: 'none', cursor: 'pointer' }}
>
์ญ์
</button>
</li>
)
})
// ์
๋ ฅ ์ปดํฌ๋ํธ
const TodoInput = memo(({ onAdd }) => {
const [text, setText] = useState('')
const handleSubmit = (e) => {
e.preventDefault()
if (!text.trim()) return
onAdd(text.trim())
setText('')
}
return (
<form onSubmit={handleSubmit} style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="ํ ์ผ์ ์
๋ ฅํ์ธ์..."
style={{ flex: 1, padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }}
/>
<button
type="submit"
style={{ padding: '8px 16px', backgroundColor: '#4CAF50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}
>
์ถ๊ฐ
</button>
</form>
)
})
// ํํฐ ์ปดํฌ๋ํธ
const TodoFilter = memo(({ filter, onChange }) => {
return (
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
{['all', 'active', 'done'].map(f => (
<button
key={f}
onClick={() => onChange(f)}
style={{
padding: '4px 12px',
backgroundColor: filter === f ? '#2196F3' : '#eee',
color: filter === f ? 'white' : '#333',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
{f === 'all' ? '์ ์ฒด' : f === 'active' ? '์งํ ์ค' : '์๋ฃ'}
</button>
))}
</div>
)
})
// ๋ฉ์ธ TodoList ์ปดํฌ๋ํธ
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'React Props ๊ณต๋ถํ๊ธฐ', done: true, createdAt: Date.now() - 86400000 },
{ id: 2, text: 'React State ๊ณต๋ถํ๊ธฐ', done: true, createdAt: Date.now() - 43200000 },
{ id: 3, text: '๋ถ๋ณ์ฑ ๊ฐ๋
์ดํดํ๊ธฐ', done: false, createdAt: Date.now() },
])
const [filter, setFilter] = useState('all')
// useCallback์ผ๋ก ํจ์ ๋ฉ๋ชจ์ด์ ์ด์
(TodoItem ์ฌ๋ ๋๋ง ๋ฐฉ์ง)
const handleAdd = useCallback((text) => {
setTodos(prev => [
...prev,
{ id: Date.now(), text, done: false, createdAt: Date.now() }
])
}, [])
const handleToggle = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
)
}, [])
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}, [])
// ํํฐ๋ง๋ ํ ์ผ ๋ชฉ๋ก
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.done
if (filter === 'done') return todo.done
return true
})
const doneCount = todos.filter(t => t.done).length
return (
<div style={{ maxWidth: '500px', margin: '40px auto', fontFamily: 'sans-serif' }}>
<h1>Todo List</h1>
<p style={{ color: '#666' }}>
{doneCount}/{todos.length} ์๋ฃ
{doneCount === todos.length && todos.length > 0 && ' ๋ชจ๋ ์๋ฃ!'}
</p>
<TodoInput onAdd={handleAdd} />
<TodoFilter filter={filter} onChange={setFilter} />
{filteredTodos.length === 0 ? (
<p style={{ color: '#999', textAlign: 'center' }}>ํ ์ผ์ด ์์ต๋๋ค.</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
)}
</div>
)
}
export default TodoList15. ์์ฃผ ํ๋ ์ค์ ๋ชจ์
์ค์ 1: State๋ฅผ ์ง์ ์์ ํ๊ธฐ
// ์๋ชป๋ ์ฝ๋
const [user, setUser] = useState({ name: 'Amy', age: 30 })
const birthday = () => {
user.age++ // ์ง์ ์์ - React๊ฐ ๊ฐ์งํ์ง ๋ชปํจ!
setUser(user) // ๊ฐ์ ์ฐธ์กฐ๋ผ์ ๋ฆฌ๋ ๋๋ง ์ ๋จ
}
// ์ฌ๋ฐ๋ฅธ ์ฝ๋
const birthday = () => {
setUser(prev => ({ ...prev, age: prev.age + 1 }))
}์ค์ 2: ๋น๋๊ธฐ ํจ์์์์ stale closure
// ๋ฌธ์ ์ํฉ
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
// ์ฌ๊ธฐ์ count๋ ํญ์ 0! (ํด๋ก์ ๊ฐ ์ด๊ธฐ๊ฐ์ ์บก์ฒ)
setCount(count + 1)
}, 1000)
return () => clearInterval(interval)
}, []) // ๋น ์์กด์ฑ ๋ฐฐ์ด
return <p>{count}</p>
}
// ํด๊ฒฐ: ํจ์ํ ์
๋ฐ์ดํธ ์ฌ์ฉ
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1) // ํญ์ ์ต์ ๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก
}, 1000)
return () => clearInterval(interval)
}, [])
return <p>{count}</p>
}์ค์ 3: ๋ ๋๋ง ์ค์ State ๋ณ๊ฒฝํ๊ธฐ
// ์๋ชป๋ ์ฝ๋ - ๋ฌดํ ๋ ๋๋ง ๋ฃจํ ๋ฐ์!
function BadComponent() {
const [count, setCount] = useState(0)
setCount(count + 1) // ๋ ๋๋ง ์ค์ State ๋ณ๊ฒฝ!
return <p>{count}</p>
}
// ์ฌ๋ฐ๋ฅธ ์ฝ๋ - useEffect๋ก ์ฌ์ด๋ ์ดํํธ ์ฒ๋ฆฌ
function GoodComponent({ initialValue }) {
const [count, setCount] = useState(initialValue)
// ๋ ๋๋ง ์ค์๋ State ๋ณ๊ฒฝํ์ง ์์
return <p>{count}</p>
}์ค์ 4: key ์์ด ๋ฆฌ์คํธ ๋ ๋๋ง
// ์๋ชป๋ ์ฝ๋ - key ์์
todos.map(todo => <TodoItem todo={todo} />)
// ์๋ชป๋ ์ฝ๋ - ์ธ๋ฑ์ค๋ฅผ key๋ก ์ฌ์ฉ (ํญ๋ชฉ์ด ์ญ์ /์ฌ์ ๋ ฌ๋ ๋ ๋ฌธ์ )
todos.map((todo, index) => <TodoItem key={index} todo={todo} />)
// ์ฌ๋ฐ๋ฅธ ์ฝ๋ - ๊ณ ์ ํ id ์ฌ์ฉ
todos.map(todo => <TodoItem key={todo.id} todo={todo} />)16. Props vs State ์ ๋ฆฌ
| ๊ตฌ๋ถ | Props | State |
|---|---|---|
| ์ ์ | ๋ถ๋ชจ์์ ์ ๋ฌ๋ฐ๋ ์ธ๋ถ ๋ฐ์ดํฐ | ์ปดํฌ๋ํธ๊ฐ ์ง์ ๊ด๋ฆฌํ๋ ๋ด๋ถ ๋ฐ์ดํฐ |
| ๋ณ๊ฒฝ ๊ฐ๋ฅ ์ฌ๋ถ | ์ฝ๊ธฐ ์ ์ฉ (์์์ด ๋ณ๊ฒฝ ๋ถ๊ฐ) | ๋ณ๊ฒฝ ๊ฐ๋ฅ (setState ํจ์ ์ฌ์ฉ) |
| ๋ณ๊ฒฝ ์ฃผ์ฒด | ๋ถ๋ชจ ์ปดํฌ๋ํธ | ์ปดํฌ๋ํธ ์์ |
| ๋ณ๊ฒฝ ์ ๋ฆฌ๋ ๋๋ง | ๋ณ๊ฒฝ๋๋ฉด ์์ ๋ฆฌ๋ ๋๋ง | ๋ณ๊ฒฝ๋๋ฉด ํด๋น ์ปดํฌ๋ํธ ๋ฆฌ๋ ๋๋ง |
| ์ด๊ธฐ๊ฐ | ๋ถ๋ชจ๊ฐ ์ ๋ฌํ ๊ฐ | useState()์ ์ธ์ |
๋ง์น๋ฉฐ
Props์ State๋ React์ ํต์ฌ์ด๋ค. ์ด ๋ ๊ฐ์ง๋ฅผ ์ ๋๋ก ์ดํดํ๋ฉด React๋ก ์ด๋ค UI๋ ์ค๊ณํ ์ ์๋ค. ํนํ ๋ค์ ์ธ ๊ฐ์ง๋ฅผ ํญ์ ๋ช ์ฌํ์.
- Props๋ ์ฝ๊ธฐ ์ ์ฉ, ์์ ์ปดํฌ๋ํธ์์ ์ ๋ ์์ ํ์ง ์๋๋ค.
- State๋ ํญ์ ๋ถ๋ณ์ฑ์ ์ ์งํ๋ฉฐ ์ ๋ฐ์ดํธํ๋ค.
- ์ด์ State๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ ๋ฐ์ดํธํ ๋๋ ํจ์ํ ์ ๋ฐ์ดํธ๋ฅผ ์ฌ์ฉํ๋ค.
๋ค์ ๊ธ์์๋ React Hooks(useEffect, useRef, useMemo, useCallback, useContext)๋ฅผ ๋ณธ๊ฒฉ์ ์ผ๋ก ๋ค๋ฃฌ๋ค.
๊ด๋ จ ๊ธ
- ๐ React ์ ๋ฌธ: SPA, ์ปดํฌ๋ํธ, JSX ํต์ฌ ๊ฐ๋ โ React ๊ธฐ์ด 1ํธ: ์ฒ์ ์์ํ๋ React
- ๐ React Hooks ์์ ์ ๋ณต โ React ๊ธฐ์ด 3ํธ: useState, useEffect, ์ปค์คํ ํ
- ๐ท TypeScript ์ ๋ฌธ: JavaScript ๊ฐ๋ฐ์๋ฅผ ์ํ ํ์ ์์คํ โ React๋ฅผ TypeScript์ ํจ๊ป ์ฐ๋ ๋ฐฉ๋ฒ
