카테고리 없음

[React]customHook으로 투두리스트 편집mode를 만들어보자

아보카도 있었어! 2023. 8. 13. 23:41

투두리스트의 편집 모드에서는 편집 모드가 아닌 상태에서 지원했던 추가, 수정(할 일 체크), 삭제 기능을 모두 지원해야 했다.

동시에 수정되었지만, 저장하지 않고 편집 모드를 종료할 수 있도록 수정되기 전 투두리스트의 값도 여전히 가지고 있어야 했다.

투두리스트의 추가/삭제/수정 로직은 일반 모드/편집 모드 둘다 동일하므로, 데이터만 다르게 사용하는 로직 재사용을 위해 customHook으로 추출하기로 했다.

 

customHook 사용 전/후 코드 비교를 위해 github에 커밋한 다음 비교했다.

🔗

 

1. customHook 사용 이전

투두리스트가 가지고 있는 전체 데이터는 `data`가 객체 배열의 형태로 관리하고 있다.

`App.jsx`

function App() {
    const [data, setData] = useState([]);

    useEffect(() => {
    fetchData();
    return
    }, []);

// 처음 렌더링될 때 기존 저장돼 있던 데이터 불러옴
  async function fetchData() {
    const dataRes = await fetch("url");
    if (dataRes.ok) {
      const result = await dataRes.json();
      setData(result);
    }
  }

}

 

상태 변수가 관리하는 배열 `data` 은 추가/수정/삭제 핸들러로 변경이 가능하다.

`App.jsx`

  // 추가
  const handleAdd = (args) => {
    setData([
      ...data,
      {
        id: nextId++, // 로직과 상관없지만, 임의의 ID라고 생각하면 된다.
        content: args,
        active: true,
      },
    ]);
  };

// 수정
  const handleCheck = (id) => {
    const nextData = data.map((todo) => {
      if (todo.id === id) {
        return { ...todo, active: !todo.active };
      }
      return todo;
    });
    setData(nextData);
  };

// 삭제
  const handleRemove = (id) => {
    setData(data.filter((todo) => todo.id !== id));
  };

 

편집모드에 진입하면, `App.jsx`에서 관리되고 있는 `data` 처럼, 편집 데이터 역시 같은 로직으로 추가/수정/삭제가 되도록 하고 싶었다.

 

customHook 에서는 useState, useEffect 와 같은 컴포넌트의 최상위에서만 사용 가능한 hook을 사용할 수 있다.

 

2. 재사용 로직 확인하기

한편, 일반모드/수정모드에서 반복될(것으로 예상되는) 로직은 다음과 같다.

 

1. 하나의 데이터를 state로 관리한다.

2. 데이터 변경 핸들러가 데이터를 추가/수정/삭제한다.

 

위 로직을 가진 코드 부분을 App.jsx에서 추출하기만 하면 된다.

 

3. customHook 추출하기

customHook은 `use-`로 시작해야 하므로, useTodoHandler 라는 customHook 파일을 생성했다.

 

`useTodoHandler.js`

function useTodoHandler(initialData) {

    const [data, setData] = useState(initialData);
    
    // 추가
    const handleAdd = (args) => {
        setData([
            ...data,
            {
                id: nextId++
                content: args,
                active: true,
            },
        ]);
    };
    
    // 수정
    const handleCheck = (id) => {
        const nextData = data.map((todo) => {
            if (todo.id === id) {
                return { ...todo, active: !todo.active }
            }
            return todo
        })
        setData(nextData)
    }
	
    // 삭제
    const handleRemove = (id) => {
        setData(data.filter((todo) => todo.id !== id));
    };

    const handleEdit = (id, content) => {
        const nextData = data.map((todo) => {
            if (todo.id === id) {
                return { ...todo, content: content }
            }
            return todo
        })

        setData(nextData)
    }

    return { data, handleCheck, handleAdd, handleRemove, setData }
}

export default useTodoHandler;

생성된 useTodoHandler는 인자 `initialData`로 받아 data라는 이름의 state로 관리하고, 해당 상태 변경 핸들러를 반환한다.

 

App.jsx에서 해당 로직을 삭제하고, customHook을 import 해온다.

`App.jsx`

import useTodoHandler from "./hooks/useTodoHandler";

function App() {
// App 컴포넌트에서만 관리하던 state를 삭제하고, customHook에서 반환된 state와 핸들러를 가져온다.
   const {
    data: originData,
    setData: setOriginData,
    handleAdd,
    handleRemove,
    handleCheck,
  } = useTodoHandler(null);
  
  const [edit, setEdit] = useState({
    edit: false,
  });

  useEffect(() => {
    fetchData();
    return;
  }, []);

  async function fetchData() {
    const dataRes = await fetch("url");
    if (dataRes.ok) {
      const result = await dataRes.json();
      setOriginData(result);
    }
  }

  if (!originData) return;

  // 반환된 핸들러를 prop으로 전달한다.
  return (
	{edit.edit ? (
          <EditMode data={originData} saveData={setOriginData} />
        ) : (
          <TodoList
            data={originData}
            handleCheck={handleCheck}
            handleRemove={handleRemove}
          />
        )}
     <Footer onAdd={handleAdd} />
    </div>
  );
}

export default App;

 

4. 편집모드에 같은 로직 적용하기

편집 모드에도 같은 로직을 적용하기 위해 customHook에 인자로 일반 모드의 data를 prop으로 받아 전달하고, state와 handler를 반환받는다. 반환된 state는 modifiedData로 편집 모드에서만 사용되고, 핸들링될 객체 배열이다.

export default function EditMode({ data, saveData }) {

  const {
    data: modifiedData,
    handleRemove,
    handleCheck,
    handleAdd,
  } = useTodoHandler(data); // 편집모드에 일반모드의 data state를 초기값으로 전달

  return (
    <div className="TodoList">
      {modifiedData.map((todo) => (
        <Todo
          key={todo.id}
          todo={todo}
          handleRemove={handleRemove}
          handleCheck={handleCheck}
          handleEdit={handleEdit}
        />
      ))}
      <button onClick={() => handleAdd("")}>Add</button>
    </div>
  );
}

 

지금까지의 코드를 적용한 구현 화면이다.

편집 모드에서 추가/수정/삭제가 가능하다

5. 수정(할 일 수정) 로직 추가

지금까지는 추가/수정(할 일 체크/삭제만 가능한 상태의 편집 모드이기 때문에 항목 추가 뿐 아니라, 기존 데이터를 수정하는 로직도 추가해야 한다. 해당 로직은 기존 App.jsx에서는 사용되지 않았기 때문에 useTodoHandler에 `handleEdit`을 추가했다.

 

`useTodoHandler.js`

function useTodoHandler(initialData) {

    const [data, setData] = useState(initialData);

...
    const handleEdit = (id, content) => {
        const nextData = data.map((todo) => {
            if (todo.id === id) {
                return { ...todo, content: content }
            }
            return todo
        })

        setData(nextData)
    }

    return { data, handleCheck, handleAdd, handleRemove, setData }
}

export default useTodoHandler;

 

`EditMode.jsx`

export default function EditMode({ data, saveData }) {

  const {
    data: modifiedData,
    handleRemove,
    handleCheck,
    handleAdd,
    handleEdit,
  } = useTodoHandler(data);
  
  return (
    <div className="TodoList">
      {modifiedData.map((todo) => (
        <Todo
          key={todo.id}
          todo={todo}
          handleRemove={handleRemove}
          handleCheck={handleCheck}
          handleEdit={handleEdit}  // handleEdit prop 추가
        />
      ))}
      <button onClick={() => handleAdd("")}>Add</button>
    </div>
  );
}

 

6. 편집된 내용 App.jsx에 전달

반환된 modifiedData는 편집 모드에서만 유효하기 때문에, 이를 전체 data를 관리하고 있는 App.jsx의 data로 상태 끌어올리기로 전달해야 한다.

해당 핸들링은 사용자가 저장 버튼을 눌러 편집 모드를 벗어날 때 수행된다.

 

`EditMode.jsx`

export default function EditMode({ data, saveData }) {

  const { edit, setEdit } = useContext(editContext);

  const {
    data: modifiedData,
    handleRemove,
    handleCheck,
    handleAdd,
    handleEdit,
  } = useTodoHandler(data);
  
  useEffect(() => {
    if (edit.isSaved) {
      saveData(modifiedData);}
    return setEdit({ ...edit, isSaved: false });
   }, [edit.isSaved]);

  return (
    <div className="TodoList">
      {modifiedData.map((todo) => (
        <Todo
          key={todo.id}
          todo={todo}
          handleRemove={handleRemove}
          handleCheck={handleCheck}
          handleEdit={handleEdit}  // handleEdit prop 추가
        />
      ))}
      <button onClick={() => handleAdd("")}>Add</button>
    </div>
  );
}

 

구현된 편집 모드는 다음과 같다.

편집모드에서 수정한 내용이 일반모드에서 반영된다