inflearn logo
강의

Course

Instructor

Bite-Sized React (React.js): From Basics to Practice

9.1) Introducing useReducer

리액트 라이프 사이클 질문

Resolved

160

HK

15 asked

0

글 수정기능을 추가하고 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);

...
}

이것이 리액트 라이프 사이클 때문에 이러한 현상이 발생한것인가요?

javascript react node.js

Answer 2

0

HK

useReducer로 했는데 오류가 나지 않아 맞는 코드라고 생각했습니다.

전체 코드 올려 드렸습니다.

useState 사용 시 깃허브 코드입니다.

useState 적용 깃허브 코드

useReducer 사용 시 깃허브 코드입니다.

useReducer 적용 깃허브 코드

 

0

winterlood

원하시는 기능을 직접 구현해봤습니다. 강의 수강 이후 제가 작성한 코드를 살펴보시면 동작 원리를 충분히 이해하실 수 있을겁니다.

아래 기재해두지 않은 파일의 내용은 변동 없습니다.

 

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

HK

type별로 체크박스와 content update를 나눠서 선언을 해야 하는군요

바뀐 코드로 실행해보았는데

TodoItem.jsx의

useEffect부분을 주석처리해도 실행되는 이유가 무엇인가요?

 

useEffect(() => { setLocalContent(content); }, [content]);
 

 

0

winterlood

해당 코드의 역할은 App 컴포넌트의 content State가 변화할 경우 localContent에 동기화 시키는 역할인데, 현재로써는 localContent의 값이 먼저 변화한 이후에 content의 값이 변화하기 때문에 없어도 문제없이 동작합니다.

0

winterlood

안녕하세요 이정환입니다.

먼저 좀 더 질문을 구체화 해 주실 수 있을까요? TodoItem 컴포넌트 하나만으로는 말씀하신 상황을 구체적으로 파악하기 어려울 것 같습니다. 강의와 코드도 조금 다른 것 같구요!

대략적으로만 파악하자면 useState의 초기값으로 Props로 받은 content를 고정해두었을 때와 useEffect를 통해 content의 값이 변화할 때 setUpContent를 호출하는 것과의 차이를 물어보신 것 같은데요 useState의 초기값은 컴포넌트의 리렌더링에 반응하지 않습니다.

즉 content로 제공되는 Props의 값이 변화한다고 해서 초기값이 다시 설정되거나 하는건 아니라는거죠 그렇기 때문에 content Props의 값이 변화할 것으로 예상된다면 useEffect를 사용하는게 더 좋은 방법이 될 수 있을 것 같습니다. 물론 이는 제한된 내용을 기반으로 답변된 내용이라 정확하지 않을 수 있습니다.

0

HK

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

winterlood

안녕하세요 이정환입니다.

아하 수정 기능을 추가하고 싶으신거군요 그러나 이렇게 작성하시면 오류가 발생합니다.

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