74일차 - react tutorial 6탄

·2024년 3월 28일

react

TodoList 수정 기능 만들기!

  • UI 만들기!

const NewTodoForm = ({ addTodo: _addTodo }) => {
	const [newTodoTitle, setNewTodoTitle] = useState("");

	const addTodo = () => {
		if (newTodoTitle.trim().length == 0) return;
		const title = newTodoTitle.trim();
		_addTodo(title);
		setNewTodoTitle("");
	};

	return (
		<>
			<div className="flex items-center gap-x-3">
				<input
					className="input input-bordered"
					type="text"
					placeholder="새 할일 입력해"
					value={newTodoTitle}
					onChange={(e) => setNewTodoTitle(e.target.value)}
				/>
				<button className="btn btn-primary" onClick={addTodo}>
					할 일 추가
				</button>
			</div>
		</>
	);
};

const TodoListItem = ({ todo, removeTodo: _removeTodo }) => {
	const [editMode, setEditMode] = useState(false);
	const readMode = !editMode;

	const enableEditMode = () => {
		setEditMode(true);
	};

	const removeTodo = () => {
		_removeTodo(todo.id);
	};

	const cancleEdit = () => {
		setEditMode(false);
	};
	const commitEdit = () => {
		setEditMode(false);
	};
	
	return (
		<li className="flex items-center gap-x-3 mb-3">
			<span className="badge badge-accent badge-outline">{todo.id}</span>
			{readMode ? (
				<>
					<span>{todo.title}</span>
					<button className="btn btn-outline btn-accent" onClick={enableEditMode}>
						수정
					</button>
					<button className="btn btn-accent" onClick={removeTodo}>
						삭제
					</button>
				</>
			) : (
				<>
					<input
						className="input input-bordered"
						type="text"
						placeholder="할 일 써"
						value={todo.title}
					/>
					<button className="btn btn-accent" onClick={commitEdit}>
						수정완료
					</button>
					<button className="btn btn-accent" onClick={cancleEdit}>
						수정취소
					</button>
				</>
			)}
		</li>
	);
};

const TodoList = ({ todos, removeTodo }) => {
	return (
		<>
			{todos.length == 0 ? (
				<h4>할 일 없음</h4>
			) : (
				<>
					<h4>할 일 목록</h4>
					<ul>
						{todos.map((todo) => (
							<TodoListItem key={todo.id} todo={todo} removeTodo={removeTodo} />
						))}
					</ul>
				</>
			)}
		</>
	);
};

const App = () => {
	const [todos, setTodos] = useState([]);
	const [lastTodoId, setLastTodoId] = useState(0);

	const addTodo = (title) => {
		const id = lastTodoId + 1;

		const newTodo = {
			id,
			title
		};
		setTodos([...todos, newTodo]);
		setLastTodoId(id);
	};

	const removeTodo = (id) => {
		const newTodos = todos.filter((todo) => todo.id != id);
		setTodos(newTodos);
	};

	return (
		<>
			<NewTodoForm addTodo={addTodo} />
			<hr />
			<TodoList todos={todos} removeTodo={removeTodo} />
		</>
	);
};
  • 기능 구현하기

  • 입력할 수 있게 onChange함수를 이용하기!

이 코드는 JavaScript의 함수입니다. 이 함수는 modifyTodo라고 하며, 할 일 목록에서 특정 항목의 제목을 수정하는 기능을 합니다. 여기서 todos는 할 일 목록을 나타내는 배열이고, 각 할 일 항목은 최소한 idtitle 속성을 가지고 있다고 가정합니다. setTodostodos 배열을 업데이트하기 위한 함수로 보입니다. 아마도 React의 상태 업데이트 함수일 가능성이 높습니다.

이 함수는 두 개의 매개변수를 받습니다:

  • id: 수정하려는 할 일 항목의 식별자입니다.
  • title: 해당 할 일 항목에 새로 할당하려는 제목입니다.

함수 내부에서는 다음과 같은 과정을 통해 작동합니다:
1. todos 배열을 map 함수를 사용해 순회합니다. map 함수는 배열의 각 요소에 대해 주어진 함수를 실행하고, 결과로 새 배열을 생성합니다.
2. 배열의 각 요소 (todo)에 대해, 현재 요소의 id가 매개변수로 받은 id와 같지 않은 경우 (todo.id != id), 현재 요소를 그대로 반환합니다. 즉, 해당 요소는 변경되지 않습니다.
3. 만약 현재 요소의 id가 매개변수로 받은 id와 같은 경우, 즉 수정하려는 항목이라면, 현재 todo 객체를 펼친 후 ({ ...todo }), title 속성을 새로운 title 매개변수의 값으로 덮어쓰기합니다. 이렇게 하여 해당 항목만 제목이 수정된 새로운 객체를 생성합니다.
4. 이 과정을 거쳐 생성된 새 배열 (newTodos)는 setTodos 함수를 사용하여 todos 상태를 업데이트합니다.

이 로직을 통해, 원본 todos 배열에서 특정 id를 가진 항목만 그 제목을 새로운 값으로 수정하고, 나머지 항목들은 그대로 유지합니다. React에서 이렇게 상태를 업데이트하면 해당 컴포넌트는 수정된 todos 배열을 반영하여 리렌더링됩니다.

const NewTodoForm = ({ addTodo: _addTodo }) => {
	const [newTodoTitle, setNewTodoTitle] = useState("");

	const addTodo = () => {
		if (newTodoTitle.trim().length == 0) return;
		const title = newTodoTitle.trim();
		_addTodo(title);
		setNewTodoTitle("");
	};

	return (
		<>
			<div className="flex items-center gap-x-3">
				<input
					className="input input-bordered"
					type="text"
					placeholder="새 할일 입력해"
					value={newTodoTitle}
					onChange={(e) => setNewTodoTitle(e.target.value)}
				/>
				<button className="btn btn-primary" onClick={addTodo}>
					할 일 추가
				</button>
			</div>
		</>
	);
};

const TodoListItem = ({
	todo,
	removeTodo: _removeTodo,
	modifyTodo: _modifyTodo
}) => {
	const [editMode, setEditMode] = useState(false);
	const [newTodoTitle, setNewTodoTitle] = useState(todo.title);
	const readMode = !editMode;

	const enableEditMode = () => {
		setEditMode(true);
	};

	const removeTodo = () => {
		_removeTodo(todo.id);
	};

	const cancleEdit = () => {
		setEditMode(false);
		setNewTodoTitle(todo.title);
	};
	const commitEdit = () => {
		if (newTodoTitle.trim().length == 0) return;

		_modifyTodo(todo.id, newTodoTitle.trim());

		setEditMode(false);
	};

	return (
		<li className="flex items-center gap-x-3 mb-3">
			<span className="badge badge-accent badge-outline">{todo.id}</span>
			{readMode ? (
				<>
					<span>{todo.title}</span>
					<button className="btn btn-outline btn-accent" onClick={enableEditMode}>
						수정
					</button>
					<button className="btn btn-accent" onClick={removeTodo}>
						삭제
					</button>
				</>
			) : (
				<>
					<input
						className="input input-bordered"
						type="text"
						placeholder="할 일 써"
						value={newTodoTitle}
						onChange={(e) => setNewTodoTitle(e.target.value)}
					/>
					<button className="btn btn-accent" onClick={commitEdit}>
						수정완료
					</button>
					<button className="btn btn-accent" onClick={cancleEdit}>
						수정취소
					</button>
				</>
			)}
		</li>
	);
};

const TodoList = ({ todos, removeTodo, modifyTodo }) => {
	return (
		<>
			{todos.length == 0 ? (
				<h4>할 일 없음</h4>
			) : (
				<>
					<h4>할 일 목록</h4>
					<ul>
						{todos.map((todo) => (
							<TodoListItem
								key={todo.id}
								todo={todo}
								removeTodo={removeTodo}
								modifyTodo={modifyTodo}
							/>
						))}
					</ul>
				</>
			)}
		</>
	);
};

const App = () => {
	const [todos, setTodos] = useState([]);
	const [lastTodoId, setLastTodoId] = useState(0);

	const addTodo = (title) => {
		const id = lastTodoId + 1;

		const newTodo = {
			id,
			title
		};
		setTodos([...todos, newTodo]);
		setLastTodoId(id);
	};

	const removeTodo = (id) => {
		const newTodos = todos.filter((todo) => todo.id != id);
		setTodos(newTodos);
	};

	const modifyTodo = (id, title) => {
		const newTodos = todos.map((todo) =>
			todo.id != id ? todo : { ...todo, title }
		);
		setTodos(newTodos);
	};

	return (
		<>
			<NewTodoForm addTodo={addTodo} />
			<hr />
			<TodoList todos={todos} removeTodo={removeTodo} modifyTodo={modifyTodo} />
		</>
	);
};

리액트 커스텀 훅

const NewTodoForm = ({ todoStatus }) => {
	const [newTodoTitle, setNewTodoTitle] = useState("");

	const addTodo = () => {
		if (newTodoTitle.trim().length == 0) return;
		const title = newTodoTitle.trim();
		todoStatusaddTodo(title);
		setNewTodoTitle("");
	};

	return (
		<>
			<div className="flex items-center gap-x-3">
				<input
					className="input input-bordered"
					type="text"
					placeholder="새 할일 입력해"
					value={newTodoTitle}
					onChange={(e) => setNewTodoTitle(e.target.value)}
				/>
				<button className="btn btn-primary" onClick={addTodo}>
					할 일 추가
				</button>
			</div>
		</>
	);
};

const TodoListItem = ({
	todo,
	todoStatus
}) => {
	const [editMode, setEditMode] = useState(false);
	const [newTodoTitle, setNewTodoTitle] = useState(todo.title);
	const readMode = !editMode;

	const enableEditMode = () => {
		setEditMode(true);
	};

	const removeTodo = () => {
		todoStatus.removeTodo(todo.id);
	};

	const cancleEdit = () => {
		setEditMode(false);
		setNewTodoTitle(todo.title);
	};
	const commitEdit = () => {
		if (newTodoTitle.trim().length == 0) return;

		todoStatus.modifyTodo(todo.id, newTodoTitle.trim());

		setEditMode(false);
	};

	return (
		<li className="flex items-center gap-x-3 mb-3">
			<span className="badge badge-accent badge-outline">{todo.id}</span>
			{readMode ? (
				<>
					<span>{todo.title}</span>
					<button className="btn btn-outline btn-accent" onClick={enableEditMode}>
						수정
					</button>
					<button className="btn btn-accent" onClick={removeTodo}>
						삭제
					</button>
				</>
			) : (
				<>
					<input
						className="input input-bordered"
						type="text"
						placeholder="할 일 써"
						value={newTodoTitle}
						onChange={(e) => setNewTodoTitle(e.target.value)}
					/>
					<button className="btn btn-accent" onClick={commitEdit}>
						수정완료
					</button>
					<button className="btn btn-accent" onClick={cancleEdit}>
						수정취소
					</button>
				</>
			)}
		</li>
	);
};

const TodoList = ({ todoStatus }) => {
	return (
		<>
			{todoStatus.todos.length == 0 ? (
				<h4>할 일 없음</h4>
			) : (
				<>
					<h4>할 일 목록</h4>
					<ul>
						{todoStatus.todos.map((todo) => (
							<TodoListItem
								key={todo.id}
								todo={todo}
								todoStatus={todoStatus}
							/>
						))}
					</ul>
				</>
			)}
		</>
	);
};

const useTodoStatus = () => {
	const [todos, setTodos] = useState([]);
	const [lastTodoId, setLastTodoId] = useState(0);

	const addTodo = (title) => {
		const id = lastTodoId + 1;

		const newTodo = {
			id,
			title
		};
		setTodos([...todos, newTodo]);
		setLastTodoId(id);
	};

	const removeTodo = (id) => {
		const newTodos = todos.filter((todo) => todo.id != id);
		setTodos(newTodos);
	};

	const modifyTodo = (id, title) => {
		const newTodos = todos.map((todo) =>
			todo.id != id ? todo : { ...todo, title }
		);
		setTodos(newTodos);
	};

	return {
		todos,
		addTodo,
		removeTodo,
		modifyTodo
	};
};

const App = () => {
	const todoStatus = useTodoStatus(); // 리액트 커스텀 훅

	return (
		<>
			<NewTodoForm todoStatus={todoStatus} />
			<hr />
			<TodoList todoStatus={todoStatus}/>
		</>
	);
};

immer

  • draft하면 불변성을 안지켜도 된다.!!(불변성이 깨지지 않는다!)
  • import immer, { produce } from "https://cdn.skypack.dev/immer";
const addTodo = () => {
 const newTodos = projuct(todos. draft => {
 	draft.push({id:3, title:"제목3"});
 });
};

const arr = [1,2,3];
const newArr = [...arr,4];
  • 사용법!

const App = () => {
	const [todos, setTodos] = useState([
		{ id: 1, title: "제목1" },
		{ id: 2, title: "제목2" }
	]);

	const addTodo = () => {
		// const newTodos = [...todos, {id:3, title:"제목3"}];
		const newTodos = produce(todos, (draft) => {
			draft.push({ id: 3, title: "제목3" });
		});

		setTodos(newTodos);
	};

	const modifyTodo = () => {
		// const newTodos = todos.map((todo) => todo.id != 1 ? todo : {...todo,title:"zxcv"});
		
		// 실무코드
		setTodos(produce(todos, (draft) => {
			draft[1].title = "zxcv";
		}));
	};

	const removeTodo = () => {
		// const newTodos = todos.filter((todo,index) => index != 1);
		const newTodos = produce(todos, (draft) => {
			draft.splice(1, 1);
		});
		setTodos(newTodos);
	};

	return (
		<>
			todos : {JSON.stringify(todos)}
			<hr />
			<button onClick={addTodo}>추가</button>
			&nbsp;
			<button onClick={modifyTodo}>수정</button>
			&nbsp;
			<button onClick={removeTodo}>삭제</button>
			&nbsp;
		</>
	);
};
  • 기존 TodoList에 적용해보기!

const NewTodoForm = ({ todoStatus }) => {
	const [newTodoTitle, setNewTodoTitle] = useState("");

	const addTodo = () => {
		if (newTodoTitle.trim().length == 0) return;
		const title = newTodoTitle.trim();
		todoStatusaddTodo(title);
		setNewTodoTitle("");
	};

	return (
		<>
			<div className="flex items-center gap-x-3">
				<input
					className="input input-bordered"
					type="text"
					placeholder="새 할일 입력해"
					value={newTodoTitle}
					onChange={(e) => setNewTodoTitle(e.target.value)}
				/>
				<button className="btn btn-primary" onClick={addTodo}>
					할 일 추가
				</button>
			</div>
		</>
	);
};

const TodoListItem = ({
	todo,
	todoStatus
}) => {
	const [editMode, setEditMode] = useState(false);
	const [newTodoTitle, setNewTodoTitle] = useState(todo.title);
	const readMode = !editMode;

	const enableEditMode = () => {
		setEditMode(true);
	};

	const removeTodo = () => {
		todoStatus.removeTodo(todo.id);
	};

	const cancleEdit = () => {
		setEditMode(false);
		setNewTodoTitle(todo.title);
	};
	const commitEdit = () => {
		if (newTodoTitle.trim().length == 0) return;

		todoStatus.modifyTodo(todo.id, newTodoTitle.trim());

		setEditMode(false);
	};

	return (
		<li className="flex items-center gap-x-3 mb-3">
			<span className="badge badge-accent badge-outline">{todo.id}</span>
			{readMode ? (
				<>
					<span>{todo.title}</span>
					<button className="btn btn-outline btn-accent" onClick={enableEditMode}>
						수정
					</button>
					<button className="btn btn-accent" onClick={removeTodo}>
						삭제
					</button>
				</>
			) : (
				<>
					<input
						className="input input-bordered"
						type="text"
						placeholder="할 일 써"
						value={newTodoTitle}
						onChange={(e) => setNewTodoTitle(e.target.value)}
					/>
					<button className="btn btn-accent" onClick={commitEdit}>
						수정완료
					</button>
					<button className="btn btn-accent" onClick={cancleEdit}>
						수정취소
					</button>
				</>
			)}
		</li>
	);
};

const TodoList = ({ todoStatus }) => {
	return (
		<>
			{todoStatus.todos.length == 0 ? (
				<h4>할 일 없음</h4>
			) : (
				<>
					<h4>할 일 목록</h4>
					<ul>
						{todoStatus.todos.map((todo) => (
							<TodoListItem
								key={todo.id}
								todo={todo}
								todoStatus={todoStatus}
							/>
						))}
					</ul>
				</>
			)}
		</>
	);
};

const useTodoStatus = () => {
	const [todos, setTodos] = useState([]);
	const [lastTodoId, setLastTodoId] = useState(0);

	const addTodo = (title) => {
		const id = lastTodoId + 1;

		const newTodo = {
			id,
			title
		};
		setTodos([...todos, newTodo]);
		setLastTodoId(id);
	};

	const removeTodo = (id) => {
		const newTodos = todos.filter((todo) => todo.id != id);
		setTodos(newTodos);
	};

	const modifyTodo = (id, title) => {
		const newTodos = todos.map((todo) =>
			todo.id != id ? todo : { ...todo, title }
		);
		setTodos(newTodos);
	};

	return {
		todos,
		addTodo,
		removeTodo,
		modifyTodo
	};
};

const App = () => {
	const todoStatus = useTodoStatus(); // 리액트 커스텀 훅

	return (
		<>
			<NewTodoForm todoStatus={todoStatus} />
			<hr />
			<TodoList todoStatus={todoStatus}/>
		</>
	);
};

useRef

  • useRef는 렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook입니다.
  • useRef는 저장공간 또는 DOM요소에 접근하기 위해 사용되는 React Hook이다.
  • ref는 reference를 뜻함
  • useState랑 비슷하다. 하지만 실행속도가 더 빠르다 (유지비용이 더 절약된다.)
  • useRef 는 .current 프로퍼티로 전달된 인자(initialValue)로 초기화된 변경 가능한 ref 객체를 반환합니다.
  • ref는 리랜더링이 안된다!

차이점

  • useState : 값이 변경되면 함수가 리랜더링 됨
  • useRef : 값이 변경되어도 함수가 리랜더링 되지 않음.
const App = () => {
	const formInputNoRef = useRef(null);
	const [number, setNumber] = useState("");
	const [recordNumbers, setRecordNumbers] = useState([10, 20, 30]);

	console.log(`AppCallCount : ${AppCallCount}`);
	AppCallCount++;
	
	const saveNumber = () => {
		if (number === "") {
			alert("숫자 입력해");
			return;
		}
		setRecordNumbers([...recordNumbers, number]);
		setNumber("");
		formInputNoRef.current.focus();
	};

	const li = recordNumbers.map((el, index) => (
		<li key={index}>
			{index + 1}: {el}
		</li>
	));

	return (
		<>
			<form
				onSubmit={(e) => {
					e.preventDefault();
					saveNumber();
				}}
			>
				<input
					ref={formInputNoRef}
					className="input input-bordered"
					type="number"
					placeholder="숫자 입력"
					value={number}
					onChange={(e) => setNumber(e.target.valueAsNumber)}
				/>
				&nbsp;
				<button className="btn btn-primary">기록</button>
			</form>
			<hr />
			<h3>기록 된 숫자 v1</h3>
			{JSON.stringify(recordNumbers)}
			<hr />
			<h3>기록 된 숫자 v2</h3>
			<ul>{li}</ul>
			<hr />
			<h3>기록 된 숫자 v2-2</h3>
			<ul>
				{recordNumbers.map((el, index) => (
					<li key={index}>
						{index + 1}: {el}
					</li>
				))}
			</ul>
		</>
	);
};

useEffect, 의존성 배열, 매번 실행되지만 타이밍이 늦어진다

  • useEffect란?
  • 특정부분 한번만 하고 그 뒤부터는 스킵한다. 이게 useEffect?
  • [] 의존성 배열, 배열안에 어떤 값이 들어있으면 그 값이 바뀌었을때 함수가 실행이 된다.
  • 빈배열이 들어있어야 한번만 실행이다!
  • 좀 나중에 되는놈을 활용해야 되는 때가 온다. (왜..?) ✨✨✨✨
  • useEffect를 쓰면 타이밍이 늦어진다.
  • 의존성 배열이 있을때랑 없을때의 차이만 알면 된다.
    1. 빈 배열이 없으면 useEffect 안에 있는 로직은 리렌더링 된다.
    2. 배열이 있으면 useEffect 안에 있는 로직은 한번만 실행된다.
import React, { useState, useEffect } from "https://cdn.skypack.dev/react@18";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18";

let AppCallCount = 0;

const App = () => {

	useEffect(() => {
			AppCallCount++;
			console.log(`App이 ${AppCallCount}번 실행`);
	}, []); // [] : 의존성 배열
	
	const [no,setNo] = useState(0);
	
	return (
		<>
			<button onClick={() => setNo(no + 1)} >증가 : {no}</button>
		</>
	);
};

리액트(React), useEffect, useRef, useState의 조합

import React, {
	useState,
	useRef,
	useEffect
} from "https://cdn.skypack.dev/react@18";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18";

let AppCallCount = 0;

const App = () => {
	AppCallCount++;
	console.log(`1App이 ${AppCallCount}번 실행`);

	const inputNameRef = useRef(null);
	const inputAgeRef = useRef(null);
	const [no, setNo] = useState(0);

	// setTimeout(() => inputNameRef.current.focus());
	
	useEffect(()=>{
		inputNameRef.current.focus();
	},[]);
	
	return (
		<>
			<input ref={inputNameRef} type="text" placeholder="이름" />
			<hr />
			<input ref={inputAgeRef} type="number" placeholder="나이" />
			<hr />
			<button
				onClick={() => {
					setNo(no + 1);
					inputAgeRef.current.focus();
				}}
			>
				증가 : {no}
			</button>
		</>
	);
};

자식이 리랜더링 되는건 부모 영향 없음, 부모가 리랜더링 되면 자식도 됨

  • 자식 안에서 리렌더링 되면 부모컴포넌트에게 영향x
  • 부모가 리렌더링 되면 자식 컴포넌트도 리렌더링 된다.
import React, { useState } from "https://cdn.skypack.dev/react@18";
import ReactDOM from "https://cdn.skypack.dev/react-dom@18";
let AppCallCount = 0;
let SubCallCount = 0;

const Sub = () => {
	SubCallCount++;
	console.log(`Sub ${SubCallCount}번 실행됨`);

	const [no, setNo] = useState(0);

	return (
		<>
			<button onClick={() => setNo(no + 1)}>Sub 버튼 : {no}</button>
		</>
	);
};

const App = () => {
	AppCallCount++;
	console.log(`App ${AppCallCount}번 실행됨`);

	const [no, setNo] = useState(0);

	return (
		<>
			<div style={{ border: "5px solid red", padding: 10 }}>
				<Sub />
				<hr />
				<button onClick={() => setNo(no + 1)}>App 버튼 : {no}</button>
			</div>
		</>
	);
};

TODO

  • immer어떻게 사용하는지 영상 다시보기
  • 외울게 많다. useEffect, useRef
  • 자식이 리랜더링 되는건 부모 영향 없음, 부모가 리랜더링 되면 자식도 됨 영상 다시보고 벨로그 정리할 거 정리하기

참고자료

느낀점

  • 리액트.. 처음엔 재밌었는데 오늘 useEffect랑 useRef배우면서 음.. 재미는 있지만 얘네 왜배우는거지.. 하면서 살짝 정신을 놨었다.
  • 그리고 요즘 진짜 너무너무 졸린데 이거 어떻게 할 방법이 없을까 생각이 많이든다.
  • 리액트 배우면서 외울게 뭔가 많아졌다. 마치.. 처음 자바 배울때가 생각난다.
profile
우당탕탕 연이의 개발일기

0개의 댓글