Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Toodos #4

Open
kyhui1115 opened this issue May 17, 2023 · 0 comments
Open

Toodos #4

kyhui1115 opened this issue May 17, 2023 · 0 comments

Comments

@kyhui1115
Copy link
Contributor

kyhui1115 commented May 17, 2023

배포링크

https://pre-onboarding-10th-3-8-83s724og3-pre-onboarding-10th-3-8.vercel.app/


🎨 디자인

1. Input 디자인 수정

  • 기본

image


  • 인풋 Hover

image


  • 인풋 Click 및 Typing

image



2. dropdown 디자인 구현

  • 드랍다운 내용이 길 경우 하단에 '...' 아이콘 표시

image


  • 드랍다운 검색어 요청 중 하단에 스피너 아이콘 표시

image


  • 드랍다운 검색어 Hover

image


  • 드랍다운 검색어 Click

image



🛠 기능 구현

1. 커스텀 훅 'useDebounce'을 이용한 API 요청 디바운스 처리

// useDebounce.ts

import { useCallback } from 'react';

export default function debounce() {
  let timeoutId: number;

  const useDebounce = useCallback((fn: any, ...args: any) => {
    clearTimeout(timeoutId);

    timeoutId = window.setTimeout(() => {
      fn(...args);
    }, 500);
  }, []);

  return useDebounce;
}

2. 커스텀 훅 'useDropdown'을 이용한 추천 검색어 요청

// useDropdown.ts

export const useGetDropdownList = () => {
  const [dropdownLoading, setDropdownLoading] = useState(false);
  const [dropdown, setDropdown] = useState<DropdownState>({ total: 0, dropdown: [] });
  const [page, setPage] = useState(INITIAL_INDEX);

  const getDropdown = async (query: string) => {
    if (dropdown.total && dropdown.total === dropdown.dropdown.length) return;
    try {
      if (!query) return;
      setDropdownLoading(true);

      const response = await apiRequest.get(
        `${RESOURCE}?q=${query}&page=${page}&limit=${GET_PAGE_LIMIT}`,
      );

      setDropdown((prev: DropdownState) => ({
        total: response.data.total,
        dropdown: [...prev.dropdown, ...response.data.result],
      }));
      setDropdownLoading(false);
    } catch (error) {
      console.error(error);
      alert('Something went wrong.');
      setDropdownLoading(false);
      throw new Error('API getTodoList error');
    }
  };

  return { dropdown, setDropdown, dropdownLoading, getDropdown, page, setPage };
};

3. 커스텀 훅 'useObserver'을 이용한 무한스크롤 구현

  • 작성코드
// useObserver.ts

export default function useObserver(setPage: React.Dispatch<React.SetStateAction<number>>) {
  const [isIntersecting, setIsIntersecting] = useState(false);
  const intersection = useRef<IntersectionObserver | null>(null);

  const observer = useCallback((node: any) => {
    if (intersection.current) intersection.current.disconnect();

    intersection.current = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
      if (entries[0].isIntersecting) {
        setIsIntersecting(true);
        setPage((prev: number) => prev + 1);
      }
      if (!entries[0].isIntersecting) {
        setIsIntersecting(false);
      }
    });
    if (node) intersection.current.observe(node as unknown as Element);
  }, []);

  return {
    observer,

    isIntersecting,
  };
}
  • UI
    ezgif com-gif-maker (1)
    • dropdown 검색어 마지막 요소가 관찰될 경우 다음 페이지 검색어 요청
    • 다음 페이지 검색어가 없을 경우 API 콜 방지

4. Dropdown 검색어 Todo 아이템 리스트 반영

  • 작성코드
const DropdownItem = ({ item, inputText, setInputText, setTodos, observer }: DropdownItemProps) => {
  const handleItemBackGround = (element: Element) => {
    element.classList.add('clicked-item');
    element.classList.remove('dropdown-item-hover');
  };

  const handleClick = async (item: string, e: React.MouseEvent) => {
    const element = e.target as Element;
    handleItemBackGround(element);

    const dropdownNewItem = { title: item };

    const { data } = await createTodo(dropdownNewItem);

    setTodos(prev => [...prev, data]);
    setInputText('');
  };

  return (
    <li
      ref={observer}
      className="dropdown-item dropdown-item-hover"
      onClick={e => {
        handleClick(item, e);
      }}
    >
      {highlightedText(item, inputText)}
    </li>
  );
};

export default DropdownItem;
  • UI
    ezgif com-gif-maker


⚙️ 리팩토링

1. TypeScript 적용:

  • TypeScript 적용으로 데이터의 타입을 정적으로 지정하며 관리하면서 에러를 줄였습니다.

2. ESLint, Prettier 적용:

  • ESLintPrettier 를 프로젝트에 적용해 코드 작업 중 에러를 체크하고 팀원들과 통일된 환경으로 협업할 수 있었습니다.

3. 커스텀훅을 사용한 관심사 분리:

  • useTodoHook을 통해 Todo 아이템을 추가하고 삭제하는 로직을 UI와 분리하는 방식으로 리팩토링했습니다.
// src/hooks/useTodoHook.ts
import React, { useState, useCallback } from 'react';
import { createTodo, deleteTodo } from '../api/todo';
import { Todo } from '../types/todoType';
// ...

export default function useTodoHook({ setTodos }: UseTodoHookProps) {
  const [submitLoading, setSubmitLoading] = useState(false);
  const [removeLoading, setRemoveLoading] = useState(false);

  const addTodo = useCallback(
    async (inputText: string) => {
      // Todo를 추가할 때 loading 상태 변화 (submitLoading)
      // inputText를 확인하고 Todo 아이템 생성 후 리스트에 추가
      // ...
    },
    [setTodos],
  );

  const removeTodo = useCallback(
    async (id: number) => {
      // Todo를 삭제할 때 loading 상태 변화 (removeLoading)
      // 선택된 해당 Todo 아이템을 리스트에서 제거
      // ...
    },
    [setTodos],
  );

  return { addTodo, removeTodo, submitLoading, removeLoading };
}

이로써 조금 더 추상화 된 방식으로 구현 가능

  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    addTodo(inputText);
    setInputText('');
  };

  const handleSearch = async (inputText: string) => {
    await getDropdown(inputText);
  };

  const handleRemoveTodo = () => {
    removeTodo(id);
  };

4. 커스텀훅 useDropdown에서 무한 스크롤 기능 수정:

  • 현재 가져온 아이템의 개수와 전체 아이템의 개수를 비교하며 무한 스크롤시 불필요한 API 콜을 방지했습니다.
// src/hooks/useDropdown.ts
import { useState } from 'react';
import apiRequest from '../api/index';
// ...

export const useGetDropdownList = () => {
  const [dropdown, setDropdown] = useState<DropdownState>({ total: 0, dropdown: [] });
  // ...

  const getDropdown = async (query: string) => {
    // dropdown의 전체 개수와 현재 아이템의 개수가 같으면 함수 종료
    if (dropdown.total && dropdown.total === dropdown.dropdown.length) return;
    // 아니라면 API를 실행해서 데이터를 추가
    // ...
  };

  return { dropdown, setDropdown, dropdownLoading, getDropdown, page, setPage };
};

5. axios intercepters 를 이용한 에러 처리:

  • axios intercepters 를 이용해 받은 응답의 status를 확인 후 데이터를 리턴하도록 했습니다.
// src/api/index.ts
// ...
baseInstance.interceptors.response.use(({ data, status }) => {
  if (status === 200) {
    return data;
  }
  throw new Error('something went wrong');
});
// ...

6. useFocus 의 useEffect 는 자신이 가지고 있게 함.

import { useRef, useEffect } from 'react';

const useFocus = () => {
  const focusRef = useRef<HTMLInputElement>(null);

  const setFocus = () => {
    focusRef.current && focusRef.current?.focus();
  };
// useEffect 를 가지고 있음으로써 호출하는 곳에서 작성하지 않아도 됨.
  useEffect(() => {
    setFocus();
  }, [setFocus]);

  return { focusRef, setFocus };
};

export default useFocus;

7. 어색한 동작 제거

const removeTodo = useCallback(
    async (id: number) => {
      try {
        setRemoveLoading(true);
        await deleteTodo(id);
        setTodos(prev => prev.filter(item => item.id !== id));
      } catch (error) {
        console.error(error);
        setRemoveLoading(false);
        alert('Something went wrong.');
      } finally {      // ❌finally 부분 삭제 
        // 아래 셋함수는  삭제가 성공한 이후에는 동작하지 않음.
        setRemoveLoading(false);
      } 
    },
    [setTodos],
  );
@shaqok shaqok changed the title 이슈 테스트 Toodos May 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant