📝Reducer
컴포넌트가 복잡해지면 컴포넌트의 state가 업데이트되는 다양한 경우를 한눈에 파악하기 어려워질 수 있습니다
컴포넌트 내부에 있는 state 로직을 컴포넌트 외부의 “reducer”라고 하는 단일 함수로 옮길 수 있습니다
reducer는 state를 다루는 다른 방법입니다
useState 사용한 예제
import { useState } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, setTasks] = useState(initialTasks);
function handleAddTask(text) {
setTasks([...tasks, {
id: nextId++,
text: text,
done: false
}]);
}
function handleChangeTask(task) {
setTasks(tasks.map(t => {
if (t.id === task.id) {
return task;
} else {
return t;
}
}));
}
function handleDeleteTask(taskId) {
setTasks(
tasks.filter(t => t.id !== taskId)
);
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false },
];
위 예제를 reducer를 이용해 바꿔보겠습니다
state에서 보낼 데이터 정보 추출
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
첫번째 이벤트 리스너에 있는 로직에 필요한 데이터를 선별한 후 dispatch라는 함수를 통해 보내주는 역할만 합니다
여기에서 type으로는 어떤 기능을 할 지 그리고 나머지에는 보내고 싶은 형태로 데이터를 보냅니다
reducer에 state에서 사용했던 로직 옮기기
function tasksReducer(tasks, action) {
if (action.type === 'added') {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
} else if (action.type === 'changed') {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
} else if (action.type === 'deleted') {
return tasks.filter(t => t.id !== action.id);
} else {
throw Error('Unknown action: ' + action.type);
}
}
type을 보내준 이유는 여기에서 사용하려고 함입니다 여기 예제에서는 if-else문으로 되어있지만 switch-case로 대부분 사용합니다 tasks는 기존 데이터(initialTasks)가 들어가고 action에서는 dispatch로 보낸 데이터가 들어가게 됩니다
useReducer를 이용한 예제로 변경한 코드
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false }
];
이걸 하나의 코드로 표현하면 위와 같습니다
- useReducer로 내가 만든 reducer(로직처리 부분)과 초기값을 연결시킵니다
- dispatch를 통해 해당 reducer로 사용할 값을 보냅니다
📝useRef
import { useRef } from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
ref는 참조에 많이 쓰이는 값이며 특징은 아래와 같습니다
- ref.current을 이용해 해당 ref의 current 값에 접근할 수 있습니다 이 값은 의도적으로 변경할 수 있으며 읽고 쓸 수 있습니다
- state와 달리 ref를 변경하면 다시 렌더링 되지 않습니다
- Event Handler에게만 필요한 정보이고 변경이 일어날 때 리렌더링이 필요하지 않다면, ref를 사용하는 것이 더 효율적입니다 (대부분 ref 접근은 이벤트 핸들러 안에서 일어납니다)
- React가 관리하는 DOM 요소에 접근해야 할 때 사용됩니다
import React, { useRef } from 'react';
function MyComponent() {
const inputRef = useRef(null);
// 렌더링 중에 ref.current를 읽는 경우
if (inputRef.current) {
console.log('렌더링 중에 ref.current를 읽음:', inputRef.current);
}
return <input ref={inputRef} />;
}
export default MyComponent;
useRef는 렌더링중에 읽거나 쓰기를 하지 않아야합니다
DOM 여러개 ref로 관리하기
import { useRef, useState } from "react";
export default function CatFriends() {
const itemsRef = useRef(null);
const [catList, setCatList] = useState(setupCatList);
function scrollToCat(cat) {
const map = getMap();
const node = map.get(cat);
node.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
function getMap() {
if (!itemsRef.current) {
// 처음 사용하는 경우, Map을 초기화합니다.
itemsRef.current = new Map();
}
return itemsRef.current;
}
return (
<>
<nav>
<button onClick={() => scrollToCat(catList[0])}>Tom</button>
<button onClick={() => scrollToCat(catList[5])}>Maru</button>
<button onClick={() => scrollToCat(catList[9])}>Jellylorum</button>
</nav>
<div>
<ul>
{catList.map((cat) => (
<li
key={cat}
ref={(node) => {
const map = getMap();
if (node) {
map.set(cat, node);
} else {
map.delete(cat);
}
}}
>
<img src={cat} />
</li>
))}
</ul>
</div>
</>
);
}
function setupCatList() {
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push("https://loremflickr.com/320/240/cat?lock=" + i);
}
return catList;
}
배열값이 있어서 순회하면서 화면에 출력하는데 해당 출력된 값에 ref로 접근해야 할 때 ref안에 useRef를 생성할 수 없기 때문에 useRef에 Map을 넣고 ref에서 node라고 ref정보를 담은 걸 key,value로 담아서 useRef를 관리한다
다른 컴포넌트에서 ref 참조하기
import { forwardRef, useRef } from 'react';
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
일반적으로 ref만 사용하면 다른 컴포넌트 ref에 접근할 수 없는데 forwordRef를 이용해 다른 컴포넌트의 ref값을 참조할 수 있습니다
📝useEffect
작동할 내용과 어떤 값을 감시할 지 정합니다 그 후 감시할 값이 변할 경우 useEffect에 작성한 내용이 작동하게 됩니다
변화되었는지에 대해서는 Object.is라는 것을 통해 주소값이 변동되었는지로 감시합니다 그렇기 때문에 감시할 값이 Object인 경우 리렌더링할 때 Object 내용은 같아도 주소값이 계속 변동되기 때문에 바뀌었다고 판단해서 계속적인 리렌더링이 일어날 수 있습니다
- 기본적으로 Effect는 모든 commit 이후에 실행됩니다
- 컴포넌트가 렌더링 될 때마다 React는 화면을 업데이트한 다음 useEffect 내부의 코드를 실행합니다 다시 말해, useEffect는 화면에 렌더링이 반영될 때까지 코드 실행을 “지연”시킵니다
- 필요한 경우 클린업 함수 추가해서 해제해 줘야합니다
useEffect 예제
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
// isPlaying 값이 변경되었다는 걸 감지했을 때 동작
useEffect(() => {
if (isPlaying) {
console.log('video.play() 호출');
ref.current.play();
} else {
console.log('video.pause() 호출');
ref.current.pause();
}
}, [isPlaying]);
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? '일시 정지' : '재생'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
useEffect은 어떤 값이 변화되었다는 걸 감지해서 감지했을 때 안에 있는 익명함수를 실행시킵니다
useEffect 사용 방법
useEffect(() => {
// 모든 렌더링 후에 실행됩니다
});
useEffect(() => {
// 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);
useEffect(() => {
// 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);
useEffect 클린업
/** 커넥션 해제 **/
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
/** 애니메이션 해제 **/
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect가 어떤 것을 구독한다면, 클린업 함수에서 구독을 해지해야 합니다
예를 들면 useEffect가 어떤 요소를 애니메이션으로 표시하는 경우, 클린업 함수에서 애니메이션을 초기 값으로 재설정해야 합니다
클린업 예제
function App() {
const [data,setData]= useState();
const [input, setInput] = useState('')
/** clean-up 미사용 **/
// useEffect(()=> {
//
// const getData = async () => {
// const resp = await fetch('https://api.sampleapis.com/coffee/hot');
// const json = await resp.json();
// setData(json[0][input]);
// }
// getData();
//
// },[input])
useEffect(()=> {
let ignore = false;
const getData = async () => {
const resp = await fetch('https://api.sampleapis.com/coffee/hot');
const json = await resp.json();
if (!ignore) {
setData(json[0][input]);
}
}
getData();
return () => {
ignore = true;
};
},[input])
return (
<div>
{data}
<br/>
<select onChange={(e)=>{setInput(e.target.value)}}>
<option value="title">title</option>
<option value="description">description</option>
</select>
</div>
);
}
DataFetch가 있고 그 이후 그 데이터를 JSX로 보여주는데 동작과정은 아래와 같다
- useId 변경 → 언마운트로 ignore true 동작 → DataFetch → setData로 리렌더링
fetch에서 clean-up 작업을 한 이유는 예를 들면 네트워크가 안 좋은데 select box를 엄청나게 바꾸면 네트워크 요청은 여러개가 나가고 리렌더링작업은 큐처럼 쌓이게 되어 뒤늦게 {data}가 큐에 쌓인대로 API요청한 회수만큼 와따가따하게 된다 clean-up 작업을 한 경우는 언마운트시 작동함으로 전 네트워크 요청에 대해서 ignore true값을 줘서 리렌더링 안 하게 한다
동작 과정은 아래와 같다
- Select box 변경 → 1번 네트워크 요청 → 1번 네트워크 요청중에 Select box 또 변경 → 1번 네트워크 요청 결과를 await중에 clean-up 내용 작동으로 ignore가 true로 변경되어서 데이터 받아와도 setData 작동 안 해서 변경 없음 → 2번 네트워크 요청 await후 input의 변경이 없기 때문에 ignore은 false상태이기 때문에 리렌더링이 된다
useEffect 사용할 때 주의할 점이 있다
- useEffect의 경우 렌더링 된 이후에 작동하기 때문에 렌더링 전에 작동시켜야하면 서버사이드에서 해결해줘야한다
- useEffect만 사용해서 데이터를 fetch하는 경우 미리 데이터를 로드하거나 캐시하지 않기 때문에 느릴 수도 있다
위와 같은 문제점을 해결하기 위해서는 일반적으로 프레임워크에 있는 내장 데이터 패칭기능이나 오픈 소스 솔루션인 React Query, useSWR를 사용하면 네트워크 응답을 캐싱하며 네트워크를 기본적으로 최적화하니 사용하는게 좋다
useEffect 잘 사용하기
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 피하세요: 중복된 state 및 불필요한 효과
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ...
}
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ getFilteredTodos()가 느리지 않다면 괜찮습니다.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ todos 또는 filter가 변경되지 않는 한 다시 실행되지 않습니다.
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
props의 todos랑 filter랑 변경되면서 다시 해당 함수인 TodoList를 실행하기 때문에 굳이 state로 관리할필요 없고 필요사항에만 메모이제이션해서 계산하게 하면 더욱 좋다
- 어떤 경우 메모이제이션을 해야할까?
- 전체적으로 기록된 시간이 상당한 양(예: 1ms 이상)으로 합산되면 해당 계산을 메모이제이션하는 것이 좋습니다가장 정확한 시간을 얻으려면 프로덕션용 앱을 빌드하고 사용자가 사용하는 것과 같은 기기에서 테스트해야합니다
function App() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// 위 useState 값이 변경되면서 전체를 실행하기 때문에 state로 상태관리를 할 필요도 없다
const fullName = firstName + ' ' + lastName;
// 🔴 피하세요: 중복된 state 및 불필요한 Effect
//const [fullName, setFullName] = useState('');
//useEffect(() => {
// setFullName(firstName + ' ' + lastName);
//}, [firstName, lastName]);
return (
<div>
<input type="text" onChange={(e)=>setFirstName(e.target.value)} />
<input type="text" onChange={(e)=>setLastName(e.target.value)} />
{fullName}
</div>
);
}
입력따위로 set하는 경우가 따로 있지 않는 경우는 state로 만들 필요가 없다
// ProfilePage 컴포넌트: userId를 prop으로 받아서 Profile 컴포넌트에 전달
function ProfilePage({ userId }: {userId:string}) {
return (
<Profile
userId={userId}
key={userId} // userId가 변경될 때마다 새로운 Profile 컴포넌트가 마운트됨
/>
);
}
// Profile 컴포넌트: userId를 prop으로 받아서 사용
function Profile({ userId }:{userId:string}) {
const [comment, setComment] = useState('');
return (
<div>
<h1>Profile for user: {userId}</h1>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Leave a comment"
/>
</div>
);
}
function App() {
const [userId, setUserId] = useState('user1');
return (
<div>
<button onClick={() => setUserId('user1')}>User 1</button>
<button onClick={() => setUserId('user2')}>User 2</button>
<ProfilePage userId={userId} />
</div>
);
}
userId값이 바뀌어서 props값이 달라지면서 리렌더링이 되는데 일반적으로 React는 동일한 컴포넌트가 같은 위치에 렌더링 될 때 state를 보존하기 때문에 comment값은 초기화가 안 됩니다 그렇기 때문에 key를 이용해 서로 다른 컴포넌트라는 걸 인식시켜줘야합니다
일반적으로 props가 계속 바뀌면 key 설정을 해주는게 맞습니다
function List({ items }: {items: Array<any>}) {
// const [selection, setSelection] = useState<any>(null);
// 🔴 피하세요: Effect에서 prop 변경 시 state 조정하기 (방법1)
// useEffect(() => {
// setSelection(null);
// }, [items]);
// 더 좋습니다: 렌더링 중 state 조정 (방법2)
// const [prevItems, setPrevItems] = useState(items);
// if (items !== prevItems) {
// setPrevItems(items);
// setSelection(null);
// }
// return (
// <div>
// <ul>
// {items.map(item => (
// <li key={item.id} onClick={() => setSelection(item)}>
// {item.name}
// </li>
// ))}
// </ul>
// <div>
// Selected: {selection ? selection.name : 'None'}
// </div>
// </div>
// );
const [selectedId, setSelectedId] = useState(null);
// ✅ 최고예요: 렌더링 중에 모든 것을 계산 (방법3)
const selection = items.find(item => item.id === selectedId) ?? null;
return (
<div>
<ul>
{items.map(item => (
<li key={item.id} onClick={() => setSelectedId(item.id)}>
{item.name}
</li>
))}
</ul>
<div>
Selected: {selection ? selection.name : 'None'}
</div>
</div>
);
}
function App() {
const [items, setItems] = useState(items1);
return (
<div>
<button onClick={() => setItems(items1)}>Set Items 1</button>
<button onClick={() => setItems(items2)}>Set Items 2</button>
<List items={items}/>
</div>
);
}
key 설정을 따로 못하는 경우 같은 위치 컴포넌트의 state 초기화 시켜야할 때는 위와 같은 방법을 이용할 수 있습니다
/** bad code **/
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);
// 🔴 피하세요: 서로를 트리거하기 위해서만 state를 조정하는 Effect 체인
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);
useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);
useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);
useEffect(() => {
alert('Good game!');
}, [isGameOver]);
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}
// ...
/** right code **/
function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
// ✅ 렌더링 중에 가능한 것을 계산합니다.
const isGameOver = round > 5;
function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}
// ✅ 이벤트 핸들러에서 다음 state를 모두 계산합니다.
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}
// ...
웬만해서 Effect를 남발하지말고 Effect 없이 해결할 수 있는지 파악한다 그리고 같은 맥락의 있는 경우 Effect를 하나로 묶어주는게 더 좋다 위 예시는 여러 개의 useEffect 훅을 사용하여 상태 업데이트를 체이닝하면 비효율적이고 유지보수가 어려운 코드를 만들 수 있다는 것을 보여줍니다
import React from 'react';
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}
function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}
return (
<div>
<button onClick={handleClick}>{isOn ? 'ON' : 'OFF'}</button>
<div
onDragEnd={handleDragEnd}
style={{ width: '100px', height: '100px', backgroundColor: 'lightgray' }}
/>
</div>
);
}
export default Toggle;
import React, { useState } from 'react';
import Toggle from './Toggle';
function App() {
const [isOn, setIsOn] = useState(false);
function handleToggleChange(nextIsOn) {
setIsOn(nextIsOn);
}
return (
<div>
<Toggle isOn={isOn} onChange={handleToggleChange} />
<div>Toggle is {isOn ? 'ON' : 'OFF'}</div>
</div>
);
}
export default App;
자식이 부모에게 명령을 내리거나 데이터를 전달하려면 부모로부터 제어권(props)들을 넘겨받아야한다