리액트 라이프 사이클 질문
글 수정기능을 추가하고 useReducer로 바꿔서 했는데
글을 추가해도 새로운 내용이 나타나지 않고, 마지막 요소가 다시 추가되었습니다. [{content:"To1"}, {content:"To1"}]
확인해보니 컴포넌트에서 받은 props를 useState초기화값으로 넣으면 바뀌지 않는 사실을 알게되었습니다.
하지만 이부분을 useEffect형식으로 바꿔 적어주니 새로운 내용으로 바뀌는 것을 확인했습니다.
// props content를 useState 초기화값으로 적용
const TodoItem = ({ content, id, isDone, date, onUpdate, onDelete }) => {
const [upContent, setUpcontent] = useState(content);
...
}
////////////////////////////////////////////
// useEffect 적용
const TodoItem = ({ content, id, isDone, date, onUpdate, onDelete }) => {
const [upContent, setUpcontent] = useState("");
useEffect(() => {
if (content) {
setUpcontent(content);
}
}, [content]);
...
}
useReducer를 적용하지 않을때 props를 useState초기화값을 넣어도 잘 구동되었습니다.
// props content를 useState 초기화값으로 적용
const TodoItem = ({ content, id, isDone, date, onUpdate, onDelete }) => {
const [upContent, setUpcontent] = useState(content);
...
}이것이 리액트 라이프 사이클 때문에 이러한 현상이 발생한것인가요?
Answer 2
0
useReducer로 했는데 오류가 나지 않아 맞는 코드라고 생각했습니다.
전체 코드 올려 드렸습니다.
useState 사용 시 깃허브 코드입니다.
useReducer 사용 시 깃허브 코드입니다.
0
원하시는 기능을 직접 구현해봤습니다. 강의 수강 이후 제가 작성한 코드를 살펴보시면 동작 원리를 충분히 이해하실 수 있을겁니다.
아래 기재해두지 않은 파일의 내용은 변동 없습니다.
App.js
import Editor from "./components/Editor";
import Header from "./components/Header";
import List from "./components/List";
import "./App.css";
import { useState, useRef } from "react";
import { useReducer } from "react";
const dummyData = [
{ id: 0, isDone: true, content: "Todo1", date: new Date().getTime() },
{ id: 1, isDone: false, content: "Todo2", date: new Date().getTime() },
{ id: 2, isDone: false, content: "Todo3", date: new Date().getTime() },
];
function reducer(state, action) {
switch (action.type) {
case "CREATE":
console.log([action.data, ...state]);
return [action.data, ...state];
case "TOGGLE_ISDONE":
return state.map((todo) =>
todo.id === action.targetId ? { ...todo, isDone: !todo.isDone } : todo
);
case "UPDATE_CONTENT":
return state.map((todo) =>
todo.id === action.targetId ? { ...todo, content: action.content } : todo
);
case "DELETE":
return state.filter((todo) => todo.id !== action.targetId);
default:
return state;
}
}
function App() {
const [todos, dispatch] = useReducer(reducer, dummyData);
// const [todos, setTodos] = useState(dummyData);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
},
});
};
const onToggleIsDone = (targetId) => {
dispatch({
type: "TOGGLE_ISDONE",
targetId: targetId,
});
};
const onUpdateContent = (targetId, upContent) => {
dispatch({
type: "UPDATE_CONTENT",
targetId: targetId,
content: upContent,
});
};
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
});
};
return (
<div className="App">
<Header />
<Editor onCreate={onCreate} />
<List
todos={todos}
onToggleIsDone={onToggleIsDone}
onUpdateContent={onUpdateContent}
onDelete={onDelete}
/>
</div>
);
}
export default App;
List.jsx
import React, { useState } from "react";
import "./List.css";
import TodoItem from "./TodoItem";
const List = ({ todos, onToggleIsDone, onUpdateContent, onDelete }) => {
const [search, setSearch] = useState("");
const onChangeSearch = (e) => {
setSearch(e.target.value);
};
const getFilteredData = () => {
if (search === "") {
return todos;
}
return todos.filter((todo) =>
todo.content.toLowerCase().includes(search.toLowerCase())
);
};
const filteredTodos = getFilteredData();
return (
<div className="List">
<h4>Todo List 🌱</h4>
<input
value={search}
onChange={onChangeSearch}
placeholder="검색어를 입력하세요."
/>
<div className="todos_wrapper">
{filteredTodos.map((value, i) => {
return (
<TodoItem
key={i}
{...value}
onToggleIsDone={onToggleIsDone}
onUpdateContent={onUpdateContent}
onDelete={onDelete}
/>
);
})}
</div>
</div>
);
};
export default List;
TodoItem.jsx
import React, { useEffect, useRef, useState } from "react";
import "./TodoItem.css";
const TodoItem = ({
content,
id,
isDone,
date,
onToggleIsDone,
onUpdateContent,
onDelete,
}) => {
const editInputRef = useRef(null);
const [isEdit, setIsEdit] = useState(false);
const [localContent, setLocalContent] = useState(content);
useEffect(() => {
setLocalContent(content);
}, [content]);
return (
<div className="TodoItem">
<input
onChange={() => onToggleIsDone(id)}
type="checkbox"
readOnly
checked={isDone}
/>
{isEdit ? (
<input
type="text"
value={localContent}
ref={editInputRef}
onChange={(e) => setLocalContent(e.target.value)}
/>
) : (
<div className={!isDone ? "content" : "cancelContent"}>{localContent}</div>
)}
<div className="date">{new Date(date).toLocaleDateString()}</div>
{isEdit ? (
<>
<button onClick={() => setIsEdit(!isEdit)}>취소</button>
<button
onClick={() => {
onUpdateContent(id, localContent);
setIsEdit(!isEdit);
}}
>
완료
</button>
</>
) : (
<>
<button onClick={() => setIsEdit(!isEdit)}>수정</button>
<button onClick={() => onDelete(id)}>삭제</button>
</>
)}
</div>
);
};
export default TodoItem;
0
type별로 체크박스와 content update를 나눠서 선언을 해야 하는군요
바뀐 코드로 실행해보았는데
TodoItem.jsx의
useEffect부분을 주석처리해도 실행되는 이유가 무엇인가요?
useEffect(() => { setLocalContent(content); }, [content]);
0
해당 코드의 역할은 App 컴포넌트의 content State가 변화할 경우 localContent에 동기화 시키는 역할인데, 현재로써는 localContent의 값이 먼저 변화한 이후에 content의 값이 변화하기 때문에 없어도 문제없이 동작합니다.
0
안녕하세요 이정환입니다.
먼저 좀 더 질문을 구체화 해 주실 수 있을까요? TodoItem 컴포넌트 하나만으로는 말씀하신 상황을 구체적으로 파악하기 어려울 것 같습니다. 강의와 코드도 조금 다른 것 같구요!
대략적으로만 파악하자면 useState의 초기값으로 Props로 받은 content를 고정해두었을 때와 useEffect를 통해 content의 값이 변화할 때 setUpContent를 호출하는 것과의 차이를 물어보신 것 같은데요 useState의 초기값은 컴포넌트의 리렌더링에 반응하지 않습니다.
즉 content로 제공되는 Props의 값이 변화한다고 해서 초기값이 다시 설정되거나 하는건 아니라는거죠 그렇기 때문에 content Props의 값이 변화할 것으로 예상된다면 useEffect를 사용하는게 더 좋은 방법이 될 수 있을 것 같습니다. 물론 이는 제한된 내용을 기반으로 답변된 내용이라 정확하지 않을 수 있습니다.
0
todolist에서 content 수정기능을 추가하였습니다. 그래서 코드가 다른점 이해해주세요.
section8 강의처럼 useSate로만 사용할때 아래의 코드를 사용하면 제대로 출력됩니다.
App.jsx
function App() {
const [todos, setTodos] = useState(dummyData);
const idRef = useRef(3);
const onCreate = (content) => {
const newTodo = {
id: idRef.current++,
isDone: false,
content: content,
date: new Date().getTime(),
};
setTodos([newTodo, ...todos]);
};
const onUpdate = (targetId, upContent) => {
if (!isNaN(targetId)) {
setTodos(todos.map((todo) => (todo.id === targetId ? { ...todo, isDone: !todo.isDone } : todo)));
} else {
setTodos(todos.map((todo) => (todo.id === targetId ? { ...todo, content: upContent } : todo)));
}
};
const onDelete = (targetId) => {
setTodos(todos.filter((todo) => todo.id !== targetId));
};TodoItem.jsx
const TodoItem = ({ content, id, isDone, date, onUpdate, onDelete }) => {
const editInputRef = useRef(null);
const [isEdit, setIsEdit] = useState();
const [upContent, setUpcontent] = useState(content);
// content를 useState 초기화 선언
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
const onClickUpdateButton = () => {
onUpdate("_", upContent); // content text updated
setIsEdit(!isEdit);
};
const onChangeUpdate = (e) => {
setUpcontent(e.target.value);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} type="checkbox" readOnly checked={isDone} />
{isEdit ? (
<input type="text" value={upContent} ref={editInputRef} onChange={onChangeUpdate} />
) : (
<div className="content">{upContent}</div>
)}
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
<button onClick={onClickUpdateButton}>수정</button>
</div>
);
};
export default TodoItem;
하지만 useReducer 사용 시 todos 새로운 content가 추가되지 않고 todos의 끝 항목이 계속 복사되고 있습니다.
App.jsx
import Editor from "./components/Editor";
import Header from "./components/Header";
import List from "./components/List";
import "./App.css";
import { useState, useRef } from "react";
import { useReducer } from "react";
const dummyData = [
{ id: 0, isDone: true, content: "Todo1", date: new Date().getTime() },
{ id: 1, isDone: false, content: "Todo2", date: new Date().getTime() },
{ id: 2, isDone: false, content: "Todo3", date: new Date().getTime() },
];
function reducer(state, action) {
switch (action.type) {
case "CREATE":
console.log([action.data, ...state]);
return [action.data, ...state];
case "UPDATE":
return !isNaN(action.targetId)
? state.map((todo) => (todo.id === action.targetId ? { ...todo, isDone: !todo.isDone } : todo))
: state.map((todo) => (todo.id === action.targetId ? { ...todo, content: action.upContent } : todo));
case "DELETE":
return state.filter((todo) => todo.id !== action.targetId);
default:
return state;
}
}
function App() {
const [todos, dispatch] = useReducer(reducer, dummyData);
const idRef = useRef(3);
const onCreate = (content) => {
dispatch({
type: "CREATE",
data: { id: idRef.current++, isDone: false, content: content, date: new Date().getTime() },
});
};
const onUpdate = (targetId, upContent) => {
console.log(targetId);
dispatch({
type: "UPDATE",
targetId: targetId,
upContent: upContent,
});
};
const onDelete = (targetId) => {
dispatch({
type: "DELETE",
targetId: targetId,
});
};
return (
<div className="App">
<Header />
<Editor onCreate={onCreate} />
<List todos={todos} onUpdate={onUpdate} onDelete={onDelete} />
</div>
);
}
export default App;
TodoItem.jsx
import React, { useEffect, useRef, useState } from "react";
import "./TodoItem.css";
const TodoItem = ({ content, id, isDone, date, onUpdate, onDelete }) => {
const editInputRef = useRef(null);
const [isEdit, setIsEdit] = useState();
const [upContent, setUpcontent] = useState("");
// const [upContent, setUpcontent] = useState(content); -> 이렇게 사용하고 useEffect를 사용하지 않으면 새로운 content가 추가되지 않고 todos의 끝 항목이 계속 복사됩니다.
useEffect(() => {
if (content) {
setUpcontent(content);
}
}, [content]);
const onChangeCheckbox = () => {
onUpdate(id);
};
const onClickDeleteButton = () => {
onDelete(id);
};
const onClickUpdateButton = () => {
if (!isDone) {
onUpdate("_", upContent); // content text updated
setIsEdit(!isEdit);
}
};
const onChangeUpdate = (e) => {
setUpcontent(e.target.value);
};
return (
<div className="TodoItem">
<input onChange={onChangeCheckbox} type="checkbox" readOnly checked={isDone} />
{isEdit ? (
<input type="text" value={upContent} ref={editInputRef} onChange={onChangeUpdate} />
) : (
<div className={!isDone ? "content" : "cancelContent"}>{upContent}</div>
)}
<div className="date">{new Date(date).toLocaleDateString()}</div>
<button onClick={onClickDeleteButton}>삭제</button>
<button onClick={onClickUpdateButton}>수정</button>
</div>
);
};
export default TodoItem;
0
안녕하세요 이정환입니다.
아하 수정 기능을 추가하고 싶으신거군요 그러나 이렇게 작성하시면 오류가 발생합니다.
TodoItem 컴포넌트에서 onUpdate 함수를 호출하면서 id 값을 전달하지 않는 경우 App 컴포넌트에서는 어떤 데이터를 수정해야 할지 알 수 없습니다.
예를 들어 3번 일기에서 content를 "aaa"로 수정한 다음 수정 버튼을 클릭하면 App 컴포넌트의 onUpdate에서는 몇번 일기가 수정되어야 하는지 어떻게 알 수 있나요?
또 지금처럼 하나의 Action Type에 두개 이상의 동작을 정의하는것은 바람직한 방식이 아닙니다. 따라서 지금과 같은 방식 보다는 useReducer에 Action Type을 하나 추가해 작업하시는게 좋아보입니다. UPDATE_CONTENT 로 추가하면 괜찮을 것 같습니다.
추가로 질문 가이드라인 확인을 부탁드립니다😃
프로젝트에서 발생한 이슈의 정확한 원인을 파악하려면 전체 코드가 필요합니다. 따라서 현재로써는 더 구체적인 답변을 드릴 수 없어 아쉽습니다 ... ㅠㅠ
여기서의 전체 코드는 파일 자체를 말씀드리는 것으로 깃허브 OR 코드 샌드박스 등의 수단을 이용해 링크로 전달해주시면 구체적으로 살펴볼 수 있습니다.
프론트엔드 학습 수준 문의
0
19
2
리액트 챕터별 코드에서 eslint 설정파일이 없어요
0
36
2
데이터 로딩중 화면만 계속 나와요!!
0
48
2
퍼블리셔일경우 어느정도 수준까지 강의를 들어야할까요
0
73
2
이후의 커리큘럼 문의
0
99
2
실슬환경 설정에서 save후 console.log 부분이 새로고침이 안되는현상입니다.
0
48
2
최적화 관련 질문있습니다 (useMemo 등)
0
82
3
프로바이더 컴포넌트의 위치는 어떤 기준인가요?
1
78
3
Date 객체에 관련하여 질문드립니다.
0
81
2
리액트 개정판 교재 질문
0
57
2
예제코드가 안나와요!
0
73
2
select a variant 선택에서 javascript와 javascript+react compiler 중 무엇을 선택해야하나요? com
0
102
2
onMouseEnter 관련 문의 드립니다
0
88
3
배열의 렌더링 관련 질문 드립니다.
0
70
2
2:40초 refObj를 콘솔로 출력시 오류가 발생합니다.
0
109
2
TS, 리액트 강의중에 뭘 먼저 수강하는게 좋을까요?
0
131
2
useCallback 적용한 onCreate, onUpdate, onDelete 함수..
0
66
1
vs code 자동완성관련 문의
0
105
2
91강 useEffect내에서 상태변화함수 호출시 발생하는 에러
1
173
2
87강 필터 함수 질문
0
65
2
useRef, useState count 비교
0
64
2
안된다고했던 이유가 무엇이었는지 모르겠습니다
0
85
2
85강에서 객체를 왜 클래스로 만들어서 new 하지 않는건지 궁금합니다.
0
73
2
투두리스트 실습을 충실히 진행한 상태에서 감정일기장 실습을 따라할 필요가 있을까요?
0
54
2

