[React+Ts] Todo

걸음걸음·2023년 4월 4일
0

Project

목록 보기
2/6
post-thumbnail

소개

제작 기한 : 2023. 03. 30 ~ 2023. 04. 04 (ver 1.0.0)
사이트 링크 / 깃허브 링크
Drag & Drop 기능으로 구현한 TodoList

Ver1.0.3

사용 스택

  • ReactJS
  • TypeScript
  • styled-components
  • Redux-toolkit
  • gh-pages

파일 구조
/src
├── /components   		# 사용한 컴포넌트 폴더
	├── Header.tsx    	# 제목, 페이지 이동 버튼 (추구 추가 예정) 등이 위치
	├── NewTodo.tsx   	# 새로운 할 일 추가 폼 컴포넌트
    ├── TodoItem.tsx  	# 할 일 컴포넌트
    ├── TodoList.tsx  	# 할 일 컴포넌트를 다루는 리스트 컴포넌트
├── /images
├── /models 	     	# 전역에서 사용되는 type을 담은 폴더
├── /store				# 리덕스 툴킷 관련 폴더
	├── store.ts
	├── todoSlice.ts
├── /UI         	  	# 스타일 관련 파일을 담은 폴더
    ├── GlobalStyle.ts
├ App.tsx
└ index.tsx

Todo type

export interface Todo {
  id: number;
  text: string;
  type: string;
  
//  important : boolean
//  clear:boolean
//  처음 type 대신 사용한 타입. 그러나 타입을 여러번 나눌 필요가 없는 구성이라 type으로 변경.
}

구현 기능

CRUD


useState를 활용하여 데이터 관리

추가

// App.tsx
  const addTodoHandler = (text: string) => {
    const newData = {
      id: data.length + 1,
      text: text,
      type: 'normal',
    };
    setData((prev) => [newData, ...prev]);
  };

// NewTodo.tsx
const NewTodo: React.FC<{ addTodo: (text: string) => void }> = ({
  addTodo
}) => {
  const inputRef = useRef<HTMLInputElement>(null);
  const submitHandler = (e: FormEvent) => {
    e.preventDefault();
    const text = inputRef.current!.value;
    addTodo(text);
    inputRef.current!.value = '';
  };
  return (
    ...
  );
};

제거

  const deleteTodoHandler = (id: number) => {
    setData((prev) => prev.filter((data) => data.id !== id));
  };

완료

  const clearTodoHandler = (id: number) => {
    setData((prev) =>
      prev.map((todo) => (todo.id === id ? { ...todo, type: 'clear' } : todo))
    );
    dataSort();
  };

Drag & Drop


드래그 앤 드롭 기능을 사용해 상태 변경이 가능하도록 구현
draggable={true} 속성을 이용하여 해당 아이템을 드래그가 가능하도록 만드는 것까진 알 수 있었지만, 어떤 이벤트를 적용하여야 원하는 기능을 구현할 수 있는지에 대해선 잘 이해하기 어려웠다.
그렇게 검색 중 간단한 드래그 앤 드롭 기능을 예제와 함께 올려주신 사이트를 발견(참고한 사이트). 해당 사이트의 예시 코드를 사용하여 언제 어떤 이벤트가 어떤 방식으로 일어나는지를 파악했다.
console.log를 사용해 이벤트의 발생 시기와 구조 파악

  • onDragStart(event:React.DragEvent<HTMLDivElement>)
    사용자가 요소를 끌어 시작할 때 발생
  • onDragOver
    드래그 요소가 드롭 타겟 위에있을 때 발생
    해당 이벤트가 없으면 드롭 이벤트가 발생하지 않는다
  • onDrop
    드래그 요소가 드롭 타겟 상에 드롭 될 때 발생
// 드래그 앤 드롭 관련 이벤트들
  const onDragHandler = (
    event: React.DragEvent<HTMLDivElement>,
    id: number
  ) => {
    event.dataTransfer.setData('id', `${id}`);
    // dataTrnasfer : 드래그 앤 드롭 작업 중에 드래그되는 데이터를 보유하는 데 사용
    // 드래그 이벤트가 발생한 아이템의 id값 저장
  };

  const onDropHandler = (
    event: React.DragEvent<HTMLDivElement>,
    type: string
  ) => {
    event.preventDefault();
    console.log('onDrop', type);
    const dataId = event.dataTransfer.getData('id');
    // drop 이벤트가 발생한 리스트의 타입 받아오기
    // 저장해두었던 id값 받아오기
    setData((prev) =>
      prev.map((todo) =>
        todo.id === Number(dataId) ? { ...todo, type: type } : todo
      )
    );
    dataSort();
  };
  const overDropHandler = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    console.log('overDrop');
  };
  • CSS 변경 추가
    드래그한 아이템이 드롭 타겟 위에 있을 때 효과를 추가
    onDragLeave (드래그 요소가 드롭 타겟 위에서 사라졌을 때 발생) 이벤트 추가
    드롭 타겟 위에 올렸을 때 선을 진하게 만드는 효과를 추가했다.
// TodoList.tsx
  const [drag, setDrag] = useState(false);
  // 타겟 위에 있을 때를 확인하기 위한 상태
  ...
  const overDropHandler = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    setDrag(true);
    // 기존 아무런 역할도 하지 않던 이벤트 함수에 drag의 상태를 변경하는 기능 추가
  };
  const leaveDragHandler = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault();
    setDrag(false);
  };
  ...

  // styled-components에 drag props 전달
  const ListTitle = styled.div<{
  ...
  drag: boolean;
}>`
  ...
  box-shadow: ${(props) =>
    props.drag ? '0 0 0 3px var(--font-line-color) inset' : ''};
  transition: all 0.3s;
  ...
`;

어려웠던 부분

타입 적용

TypeScript를 사용한 프로젝트는 처음이라, 해당하는 타입을 찾거나 적용하는 부분이 아직 어려웠다. 특히 string, number 등 기본 타입을 벗어난 이벤트나 객체의 경우 어떤 타입을 사용해야 할 지 몰라 검색의 도움을 많이 받았다.

코드 정리

데이터와 데이터를 다루는 함수를 모두 App.tsx에서 관리하고 있으며, 할 일 리스트로 '중요한 일', '해야할 일', '완료한 일' 이렇게 세 개를 두었더니 같은 코드를 여러번 반복해서 적어야 했다. 특히 데이터를 다루는 함수가 실행되는 곳은 할 일 리스트와 아이템 컴포넌트였기 때문에 함수를 모두 전달해야 해서 더 코드의 분량이 많아졌다.

//App.tsx
<TodoList
  type='important'
  title='중요한 일'
  data={important}
  clearTodo={clearTodoHandler}
  deleteTodo={deleteTodoHandler}
  onDropHandler={onDropHandler}
  overDropHandler={overDropHandler}
  onDragHandler={onDragHandler}
/>
<TodoList
  type='normal'
  title='해야할 일'
  ...
/>
<TodoList
  type='clear'
  title='완료한 일'
  ...
/>
// 같은 코드가 계속 반복 중

// TodoList.tsx
const TodoList: React.FC<{
  type: string;
  title: string;
  data: { id: number; text: string }[];
  deleteTodo: (id: number) => void;
  clearTodo: (id: number) => void;
  onDropHandler: (event: React.DragEvent<HTMLDivElement>, type: string) => void;
  overDropHandler: (event: React.DragEvent<HTMLDivElement>) => void;
  onDragHandler: (event: React.DragEvent<HTMLDivElement>, id: number) => void;
}> = ({
  type,
  title,
  data,
  deleteTodo,
  clearTodo,
  onDropHandler,
  overDropHandler,
  onDragHandler,
}) => {
// 전달 받는 props의 양이 많다

함수 옮기기

해당 코드를 줄이기 위해 App.tsx나 다른 컴포넌트에서 사용하지 않을 함수를 리스트 컴포넌트로 이동시켰다. 대신 데이터를 다루기 위해 setData와, 함수로 인해 바뀐 데이터를 다시 각 타입에 맞게 정렬하는 dataSort 함수를 props로 전달해주었다.

// App.tsx
<TodoList
  type='important'
  title='중요한 일'
  data={important}
  setData={setData}
  dataSort={dataSort}
/>
<TodoList
  type='normal'
  title='해야할 일'
  data={normal}
  ...
/>
<TodoList
  type='clear'
  title='완료한 일'
  data={clear}
  ...
/>
// TodoList.tsx
const TodoList: React.FC<{
  type: string;
  title: string;
  data: { id: number; text: string }[];
  setData: React.Dispatch<React.SetStateAction<Todo[]>>;
  dataSort: () => void;
}> = ({ type, title, data, setData, dataSort }) => {

전체 코드의 길이는 줄어들었지만 setState의 경우 부모 컴포넌트에서 자식 컴포넌트로 이동시키지 않는게 좋다는 이야기가 있어서 고민 중. 추후 작은 프로젝트지만 연습 겸 redux를 적용시킬 예정.

list 배열 화

전체 코드를 줄여도 반복되는 코드가 있는건 변하지 않아서 TodoList 또한 필요한 데이터를 배열화 시켜서 map을 적용시켰다.

//App.tsx
const typelist = [
  { type: 'important', title: '중요한 일', data: important },
  { type: 'normal', title: '해야할 일', data: normal },
  { type: 'clear', title: '완료한 일', data: clear },
];
...
{typelist.map((list) => (
  <TodoList
    type={list.type}
    title={list.title}
    data={list.data}
    setData={setData}
    dataSort={dataSort}
  />
))}

처음 원했던 대로 전체 코드의 양은 줄였지만, 이게 최적화가 된 것인지는 모르겠다. 앞으로도 공부해나가면서 계속 코드 정리를 해야 겠다.

Redux-toolkit 적용

YesOrNo 프로젝트로 연습, 이번엔 조금 더 데이터가 복잡한 해당 프로젝트에 Redux-toolkit을 적용시켰다.
slice와 store을 만들기까진 했는데, 프로젝트에서 해당 데이터를 불러오는데에서 헤맸다.

set

// todoSlice.ts
type TodoState = Todo[];
const initialState: TodoState = [];
export const todoSlice = createSlice({
  name: 'todo',
  initialState,
  reducers: {
    set: (state, action) => {
      // state = [...action.payload];
      state.push(...action.payload);
    },
    ...
  },
});

// App.tsx
const todoData = useSelector((state: RootState) => {
  return state.todo;
});

const getData = async () => {
  try {
    const data = await fetch('http://localhost:3001/data')
      .then((res) => {
        return res.json();
      })
      .then((json) => {
        dispatch(set(json));
      });
    return data;
  } catch (e) {
    return e;
  }
};

set에서 console.log(state)를 찍어봤을 땐 제대로 데이터가 맞게 들어가는데, App.tsx에서 todoData로는 데이터가 전달되지 않는 문제가 있었다. state를 기준으로 삼지 않고 아예 다른 값을 넣어버렸기 때문일까 state = [...action.payload]; 코드로는 데이터를 받아오지 못해 아래 state.push(...action.payload)로 변경. 성공적으로 데이터를 받아올 수 있었다.

업데이트 문제(del, clear)

export const todoSlice = createSlice({
  ...
    del: (state, action) => {
      return (state = state.filter((todo) => todo.id !== action.payload));
    },
    clear: (state, action) => {
      return (state = state.map((todo) =>
        todo.id === action.payload ? { ...todo, type: 'clear' } : todo
      ));
    },

값은 제대로 변경되는데, 해당 변경된 값이 수동으로 새로고침을 하지 않으면 반영되지 않는 문제 발생. 값은 제대로 변경되었기 때문에 코드에 문제가 있다곤 생각 못하고 있었는데, 함수에 return을 붙여 반환시켜주었더니 원하는대로 바로 값이 변경되었다.

업데이트 문제(update)

    update: (state, action) => {
      const { id, type } = action.payload;
      return (state = state.map((todo) =>
        todo.id === id ? { ...todo, type: type } : todo
      ));
    },

type을 변경하는 함수. 위의 clear 함수와 받아오는 값이 조금 다를 뿐, 기본 로직은 같은데 같은 방법을 써도 해당 함수는 바로 값을 변경하지 않고 수동으로 새로고침을 해야 값의 변경을 보여주었다. 이유를 알 수 없어 해매던 중, number 타입으로 받아와야 하는 값(id)이 string타입으로 전달되는 것을 발견, 해당 타입을 바꿔주었더니 원하는대로 전달되었다.
타입 때문에 제대로 업데이트가 이루어지지 않은 경우. 엄격한 비교를 사용했는데 (수동으로 새로고침을 해야하긴 했으나) 해당 아이템의 값이 변경되었다는 점이 의아함. typescript로 이러한 휴먼 에러도 방지할 수 있는지 더 찾아보아야 겠다.

업데이트 예정

  • 코드 정리
  • Redux를 사용한 상태 관리 04.13
  • 할 일 추가시 중요도 체크
  • 데이터 유지
  • 옵션 페이지 추가
  • 디자인 개선
    - 할 일 아이템을 리스트에 드래그 했을 때 CSS 변경 추가
profile
꾸준히 나아가는 개발자입니다.

0개의 댓글