인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

HK님의 프로필 이미지
HK

작성한 질문수

[2024] 한입 크기로 잘라 먹는 리액트(React.js) : 기초부터 실전까지

9.1) useReducer를 소개합니다

리액트 라이프 사이클 질문

작성

·

26

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);

...
}

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

답변 1

0

이정환 Winterlood님의 프로필 이미지
이정환 Winterlood
지식공유자

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

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

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

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

HK님의 프로필 이미지
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;

 

 

 

 

 

 

 

 

 

 

 

이정환 Winterlood님의 프로필 이미지
이정환 Winterlood
지식공유자

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

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

TodoItem 컴포넌트에서 onUpdate 함수를 호출하면서 id 값을 전달하지 않는 경우 App 컴포넌트에서는 어떤 데이터를 수정해야 할지 알 수 없습니다.

예를 들어 3번 일기에서 content를 "aaa"로 수정한 다음 수정 버튼을 클릭하면 App 컴포넌트의 onUpdate에서는 몇번 일기가 수정되어야 하는지 어떻게 알 수 있나요?

또 지금처럼 하나의 Action Type에 두개 이상의 동작을 정의하는것은 바람직한 방식이 아닙니다. 따라서 지금과 같은 방식 보다는 useReducer에 Action Type을 하나 추가해 작업하시는게 좋아보입니다. UPDATE_CONTENT 로 추가하면 괜찮을 것 같습니다.

추가로 질문 가이드라인 확인을 부탁드립니다😃

프로젝트에서 발생한 이슈의 정확한 원인을 파악하려면 전체 코드가 필요합니다. 따라서 현재로써는 더 구체적인 답변을 드릴 수 없어 아쉽습니다 ... ㅠㅠ

여기서의 전체 코드는 파일 자체를 말씀드리는 것으로 깃허브 OR 코드 샌드박스 등의 수단을 이용해 링크로 전달해주시면 구체적으로 살펴볼 수 있습니다.

HK님의 프로필 이미지
HK

작성한 질문수

질문하기