원티드 프리온보딩 챌린지 2주차 과제 1/3

HR.lee·2022년 8월 16일
1

원티드

목록 보기
5/9

진행중!

깃헙 활용하기!

  • 그동안 브랜치 1개 = 커밋 1개로 만들고 바로바로 풀을 올리고 삭제했었는데 이렇게하면 이슈에 정리해 올리기 무척 귀찮다는걸 알았다.
  • 그리고 어차피 커밋메시지와 풀리퀘 상세내역이 있기 때문에 혼자 일할때 브랜치 이름까지 같이 맞출 필요도 없는 것 같고... 그래서 코드리팩토링 부분을 커다랗게 2개로 나누어 작업하기로 했다.

1. query branch

  1. react-query의 기능 개선을 담당하는 브랜치
    해당 이슈

  2. 개념 정리

1) server state

  • server state는 server에서 받아오는 state를 얘기하는게 아니라 얘를 받아서 스토어에 넣은, 리액트 앱 내에서 관리되는 state를 이야기한다. 리액트쿼리에서는 스토어 부분이 훅 안에 들어가있어서 잘 안보이지만 쿼리키나 쿼리 클라이언트로 내부에서 작동하고 있음을 알 수 있다.

  • 따라서 server state도 react state고 useMutation만 state를 바꿀 수 있는게 아니다. mutation은 server로 보내는 state를 바꾸는거고 리액트 내의 state는 query key = store에서 관리될 수 있다.

2) query key

  • query key는 배열 안에 담긴 문자열 형태로만 사용이 된다.(버전4에서 규칙으로 고정됨)

  • 이 키는 react-query 안에서 직렬화를 거쳐서 1개의 번들링된거 같은 형태가 된다. 그리고 키 : 값의 형태로 데이터를 담아두었다가, 다시 뿌려주고, 수정해주고 하는 역할을 한다. 리덕스를 왜 뜯어보라고 하셨는지 알겠다.

  • query key는 key <-- 이다. 오타가 나면 안되고 계속 쓰인다. 따라서 action 지정해줄때처럼 상수로 만들어서 따로 관리하는 것이 좋다.

  • 객체에 상수를 담는 법 : as const!! 이렇게 하면 진짜 상태트리처럼 하나로 묶을 수 있다!

const Keys = {
all = ['todos'] as const,
detail: (id: number) => [...todoKeys.details(), id] as const
}

print : all ==> ['todos']

3) error

  • 모든 useQuery에서 onError를 처리할 필요는 없다. 공통적인 부분은 빼서 에러바운더리에서 처리하고 뭔가 추가하고 싶은거에 추가해주기

  • onError에서 하고 싶은 일 : 백엔드에서 보내온 에러메시지를 띄워주고 navigate를 쓰게하기!

  • 리셋 함수에다가 공통적인걸 넣어주고 QueryErrorResetBoundary 레이어를 위에 덮어쓰는 방식으로 사용할 수 있다. 이 안에는 에러 바운더리를 넣어야 하는데 이건 리액트쿼리에서 제공하는게 아닌, 리액트에서 제공하는 클래스형 컴포넌트의 예시이다.

함수형으로 만들기 : https://gist.github.com/andywer/800f3f25ce3698e8f8b5f1e79fed5c9c

  • useErrorBoundary는 index.tsx에서 쿼리클라이언트 생성할때 캐시에다가 걸어준 온에러에 이 쿼리의 에러들을 위임하는 훅이다.

  • 일단은 react-error-boundary를 써서 빌드한 후, 시간이 남으면 자체제작하는 방향으로 가기로 했다.

4) rules of hooks

  • 리액트 쿼리는 커스텀 훅 형태로 이루어져 있다.
  • 즉 커스텀 훅의 규칙을 따른다.
  1. 훅은 use_ 이름을 붙여서 만든다.
  2. 반복문, 조건문 혹은 중첩된 함수 내에서 Hook을 호출해서는 안된다. <-- 이래서 안됐구나
  3. Hook은 React 함수 내에서 호출해야 한다. (커스텀 훅 안에서 호출하는건 괜찬)

실제 코드에 적용된 것

1. Query Key를 상수화

before

  • 이렇게 문자열로 바로 넣어서 사용했다.
export function getToDos() {
  return useQuery(
    ["todos"],
    () => ToDosAPI.getToDos().then((response) => response.data),
    {
      useErrorBoundary: (error: AxiosError) =>
        error instanceof AxiosError &&
        error.response?.status !== undefined &&
        error.response.status >= 500,
    }
  );
}

after

  • 멘토님 코드를 잘 가져와서...
const Keys = {
  all: ["todos"] as const,
  details: () => [...Keys.all, "detail"] as const,
  detail: (id: string) => [...Keys.details(), id] as const,
};

export function useGetToDos() {
  return useQuery(
    Keys.all, <--- 이렇게 넣어준다!
    () => ToDosAPI.getToDos().then((response) => response.data),
    {
      onSuccess: () => {
        console.log("로딩 완료");
      },
      useErrorBoundary: (error: AxiosError) =>
        error instanceof AxiosError &&
        error.response?.status !== undefined &&
        error.response.status >= 500,
    }
  );
}
    1. 오타위험이 줄어들고
    1. 나중에 키를 바꾸기가 쉽다. 한곳에서 관리하니까

2. use+ 리네이밍

  • 앞에 use를 하나씩 붙여주었다.
useGetToDos() 
useGetToDoById(id)
useCreateTodo()
useUpdateToDo(id)
useDeleteToDo(id)
  • 그런데 이걸 하면서 보니까 다들 당연하게 todo라고 사용하고 있었다! toDo는 마이너한 선택이었다... 다음에는 좀더 범용적인 이름을 사용해보자.

3. Detail Page 전면 리팩토링

before

  • 일단은 mutate를 한거부터가 잘못되었다.
export function getToDoById() {
  const navigate = useNavigate();
  const queryClient = useQueryClient();
  return useMutation((id: string) => ToDosAPI.getToDoById(id), {
    onSuccess: () => queryClient.invalidateQueries(["todo"]),
    onError: (error: AxiosError) => {
      if (error !== undefined && error instanceof AxiosError) {
        alert(Object.values(error?.response?.data)[0]);
        navigate("/todo");
      }
    },
  });
}
  • 그리고 뷰단에서는 서버가 준 데이터를 실시간으로 변화시키기 위해 리코일 스테이트 안에다가 넣고 삭제버튼 누를때마다 클리어되게 하고 있었다.

// store.ts
export const toDoDetail = atom<ToDoDetail | null>({
  key: "toDoDetail",
  default: null,
}); <--- 여기서 리코일을 해놓고



// card.tsx
<el.Button isSmall={true} onClick={() => navigate(`/todo/${data.id}`)}>
          상세
        </el.Button> <--- 여기서 주소만 변경




// detail.tsx
  const location = useLocation();
  const id = location.pathname.split("/")[2];
  const [detail, setDetail] = useRecoilState(toDoDetail); <-- 받아와서 리코일에 넣어줌
  
  useEffect(() => {
    id !== undefined && <-- 여기서 주소창에 아이디값이 있을때만 돌리기
      mutateAsync(id).then((response) => setDetail(response.data));
  }, [location]);



// card.tsx

  const [cleanData, setCleanData] = useRecoilState(toDoDetail); <- 이름만 바꾼 같은 데이터

  const deleteHandler = () => { 
    console.log("삭제완료");
    deleteById.mutateAsync();
    setCleanData(null); <-- 카드에서 삭제를 하면 그냥 얘를 밀었다.
  };
  • 리액트쿼리의 특성을 완벽하게 무시하고 돌아가는 코드였다.

  • 다행히 강의를 잘 듣고 리액트쿼리에 대한 관점을 다시 잡을 수 있었다.

  • 리액트쿼리는 조건이 충족되는 한 자동으로 계속 돌아간다. "이 비동기 요청은 state가 x인 경우의 결과물이다" 라고 생각하면서 state를 변경해서 리액트쿼리가 따라오게 만들어주자.

after

  • 일단 전역 에러를 설정해주고 주소가 아닌 정식으로 useParams를 이용해서 쿼리스트링을 해주었다.
const useParams = new URLSearchParams();

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      suspense: true,
      staleTime: 1000 * 60,
    },
  },
  queryCache: new QueryCache({
    onError: (error) => {
      if (error !== undefined && error instanceof AxiosError) {
        console.log(Object.values(error?.response?.data)[0]);
        useParams.delete("id");
      }
    },
  }),
});
  • 그리고 그거에 맞춰서 카드와 디테일페이지에서도 쿼리스트링 형태로 주소가 가도록 했다.

//card.tsx

<el.Button
isSmall={true}
onClick={() => navigate(`/todo/?id=${data.id}`)}
  >
    상세
</el.Button>


// detail.tsx
  const { search } = useLocation();

  const [searchParams, setSearchParams] = useSearchParams();
  const idState = searchParams.get("id") || "";
  const [id, setId] = React.useState("");
  const { data, isFetching, isLoading } = useGetToDoById(id);

  React.useEffect(() => {
    setId(idState);
  }, [data, search]);
  • 이렇게 useState값을 하나 만든 후, 주소창이 변경되면 자동으로 setId를 시킨다. 그리고 메인에서는 에러가 발생할 경우 URLSearchParams에 있는 결과값을 삭제해주도록 했다.

  • 과제의 조건인 -> 뒤로가기를 했을때 순서대로 한일들을 볼수 있을것

  • 그리고 중간에 삭제된 카드가 있더라도 오류가 나지 않고 새로고침도 되지 않고 데이터의 정합성을 유지할 것, 2가지를 지키기 위해서였다.

      <div className="w-1/2">
        <DateBox>
          <el.Text className="text-lg md:text-2xl ml-3 dark:text-white self-end">
            What To Do!
          </el.Text>

          <div className="flex flex-col items-end dark:text-white">
            <el.Text className="text-sm">
              created :{" "}
              {!isFetching && data && idState && id
                ? formatDistanceToNow(new Date(data?.data.data.createdAt), {
                    addSuffix: true,
                    includeSeconds: true,
                  })
                : "생성한 날짜"}
            </el.Text>

            <el.Text className="text-sm">
              updated :{" "}
              {!isFetching && data && idState && id
                ? formatDistanceToNow(new Date(data?.data.data.updatedAt), {
                    addSuffix: true,
                    includeSeconds: true,
                  })
                : "수정한 날짜"}
            </el.Text>
          </div>
        </DateBox>

        <div className="w-full bg-white p-4 m-2 overflow-y-scroll h-32">
          {isFetching ? (
            <el.Text variant="text">삭제중...</el.Text>
          ) : (
            <>
              <el.Text variant="title">{data?.data.data.title}</el.Text>
              <el.Text variant="text">{data?.data.data.content}</el.Text>
            </>
          )}
        </div>
      </div>
  • 리코일을 안썼는데도 isFetching을 이용하니 "삭제중..." 표시를 띄워줄수 있어서 css도 깨지지 않고 깔끔하게 처리되었다.

  • useQuery를 이용해 다시 제작한 useGetToDoById

  • 이상하게 onSuccess를 하면 1초에 한번씩 서버로 가는데 아직 이유를 찾지 못했다.

  • 다른 에러의 경우 토큰이 없으면 자동으로 navigate를 해주므로 이 훅에 대해서만 useErrorBoundary를 무효화하고 메인페이지로 돌아가도록 해주었다.

export function useGetToDoById(id: string) {
  const navigate = useNavigate();
  const queryClient = useQueryClient();

  return useQuery(Keys.detail(id), () => ToDosAPI.getToDoById(id), {
    retry: 1,
    useErrorBoundary: false,
    // onSuccess: () => {
    //   queryClient.invalidateQueries(Keys.detail(id));
    // },
    onError: (error: AxiosError) => {
      if (error !== undefined && error instanceof AxiosError) {
        queryClient.invalidateQueries(Keys.detail(id));
        navigate("/todo");
      }
    },
  });
}

4. ErrorBoundary 설정

before

  • 에러바운더리에 쿼리캐시 설정도 안하고 리셋함수를 달지 않아서 동작을 안하고 있었다. 그리고 그래서 onError를 하나하나 달아주었었다...!

after

  • react-error-boundary를 설치해서 에러바운더리와 서스펜스를 달고 느린3G 네트워크에서 상단바(디테일), 투두리스트가 따로따로 잘 로딩되는 것을 확인했다.

  • 에러가 하단에서 상단으로 전파되는 것도 체크한 후 일반 쿼리들에 useBoundary를 달아주고 onError함수들을 제거했다.

  • 스피너에 onClick기능을 달긴 했지만 역시 버튼 있는 페이지를 하나 만들어주는게 좋겠다. 스피너는 계속 돌아가니까.

return (
    <>
      <Layout>
        <QueryErrorResetBoundary>
          {({ reset }) => (
            <ErrorBoundary
              onReset={reset}
              fallbackRender={({ resetErrorBoundary }) => (
                <el.Spinner onClick={() => resetErrorBoundary()} />
              )}
            >
              <el.HiddenBox close={close}>
                <React.Suspense fallback={<el.Spinner />}>
                  <div className="flex flex-row w-full">
                    <CreateToDoForm />
                    <DetailPage />
                  </div>
                </React.Suspense>

                <button
                  className="mt-5 -mb-10 flex 
                  justify-center items-center mx-auto w-8 
                  hover:fill-yellow-300 active:fill-yellow-500 
                  active:scale-110"
                  onClick={() =>
                    window.scrollY > 120 && setClose((state) => !state)
                  }
                >
                  <Arrow />
                </button>
              </el.HiddenBox>

              <React.Suspense fallback={<el.Spinner />}>
                <div className="flex p-10">
                  {data?.data && <List {...data} />}
                </div>
              </React.Suspense>
            </ErrorBoundary>
          )}
        </QueryErrorResetBoundary>
      </Layout>
    </>
  );

5. 상단바 useEffect clean up

before

  • 간단하게 만든 스크롤이 일정 구간일때 밑으로 내려오게 하는 함수다.
  const [close, setClose] = React.useState(false);

  const handleTopSideBar = () => {
    window.addEventListener("scroll", handleTopSideBar);
    if (window.scrollY < 120) {
      return setClose(false);
    }
  };
  useDebounce(handleTopSideBar, 700);
  • 그런데 리액트 쿼리 리팩토링을 하다가 어느 순간부터 네트워크 요청이 폭증하는 것을 확인했고, 원인을 찾다가 useEffect 관련한 설명을 보았다.

  • useEffect에 event listener를 달았을때는 사라지지 않고 계속 남아 장기적으로 메모리에 해를 끼치기 때문에 반드시 clean up 기능으로 제거해 주어야 한다고 한다!

after

  const [close, setClose] = React.useState(false);

  const handleTopSideBar = () => {
    if (window.scrollY < 120) {
      return setClose(false);
    }
  };
  const Throttle = useThrottle(handleTopSideBar, 700);

  React.useEffect(() => {
    window.addEventListener("scroll", handleTopSideBar);
    return () => {
      window.removeEventListener("scroll", Throttle);
    };
  }, []);
  • 그래서 이렇게 바꿔주었다. 문제는 다른 곳에서 찾긴 했지만 그래도 개선이니까 올리기.

레이아웃 관련 개선

1. form유효성검사 + 테스트 철저히 해서 빠져나가는거 없게 하기

  1. 로그인 / 회원가입 폼
  • 로그인 폼의 경우 useEffect로 변화를 감지해서 오류메시지를 밑에 띄워주는 형식을 쓰고 있었는데, 열심히 테스트를 돌리다보니 루프홀이 있는 것을 발견했다.
  • 한번 Submit 모드에 들어감 -> 오류가 남 -> 오류가 없는데도 버튼이 여전히 disable 상태
  • 이유
setIsSubmitting((state) => !state);
  • 무지성으로 state -> state를 했기 때문에 왔다갔다 하면서 꺼졌던 것...
  • 얘를 true로 변하게 하고 마지막 콜백실행용 useEffect에 !isError라는 조건을 달아주었다.
  useEffect(() => {
    if (isSubmitting && !isError) {
      callback();
    }
  }, [isSubmitting]);
  1. CreateToDoForm
  • 저번에 여러가지 시행착오를 겪으면서 처리해놓았었다. 버튼을 없애고 엔터로만 submit할 수 있게 했다.
  • 조건 1 : 제목과 내용을 모두 입력하지 않은 경우 - 둘다에 required를 달아놓아서 브라우저가 제공해주는 기능대로 이칸도 입력해주세요 라고 뜬다. 둘다 입력하지 않았을 경우에는 제목에 ref가 달려있기에 제목으로 포커스가 간다.
  const Submits = () => {
    if (values.title === "" && createRef.current) {
      return createRef?.current.focus();
    }
    setValues({
      title: "",
      content: "",
    });
    mutateAsync(values);
  };
  • 조건 2 : 제목만 입력하고 엔터치는 경우 - 이 칸을 입력해주세요와 함께 내용칸으로 포커스가 이동

  • 조건 3 : 내용만 입력하고 엔터치는 경우 - 이 칸을 입력해주세요와 함께 제목칸으로 포커스가 이동

  • 조건 4 : 제목 칸에서 시프트 + 엔터 ==> 먹지 않음

  • 조건 5 : 내용 칸에서 시프트 + 엔터 ==> 줄바꿈이 되지만 등록시에 줄바꿈이 적용되지는 않음. 문법을 같이 전달하고 출력하려면 html 텍스트 에디터가 필요하다고 한다.

  • 혹시몰라 submit과 키보드 이벤트를 둘다 만들고 예외처리를 달아두었다.

2. 수정 / 삭제 / 제출 에 대한 확인 처리 : 모달로 컨펌 만들어주기

  1. 모달 만들기
  • 저번 프로젝트에서 만든걸 떼어왔다.
  • 구성요소 : Modal, ModalContent, useModal
  1. 붙이기
  • 에러 바운더리의 폴백에 붙이고 리셋함수(콜백)를 넘겨주기로 했다.
        <QueryErrorResetBoundary>
            {({ reset }) => (
              <ErrorBoundary
                onReset={reset}
                fallbackRender={({ error, resetErrorBoundary }) => (
                  <Modal
                    isShown={isShown}
                    hide={toggle}
                    modalContent={<el.ModalContent content={error} />}
                    contentText={"확인"}
                    callback={() => resetErrorBoundary()}
                  />
                )}
              >
  • 이렇게 모달을 넘기면 포탈형으로 하나 생성되어서 해당 에러메세지를 뱉는다.
  • 모달 내부에는 분기처리를 해주어서 수정, 삭제, 오류확인 모두 안의 메시지만 교체하여 하나로 돌려쓸 수 있게 했다.
export const Modal: React.FunctionComponent<ModalProps> = ({
  isShown,
  hide,
  modalContent,
  headerText,
  callback,
  contentText = "확인",
}) => {
  const Modal = (
    <React.Fragment>
      <Backdrop onClick={() => hide()} />
      <Wrapper>
        <StyledModal>
          <Content>
            {modalContent}
            <div className="flex flex-row divide-x ">
              <button
                className="bg-white  mx-auto flex items-center justify-center font-bold text-center w-44 max-w-md"
                onClick={callback}
              >
                {contentText}
              </button>
              <button
                onClick={contentText === "확인" ? callback : hide}
                className="bg-white  mx-auto flex items-center justify-center font-bold text-center w-44 max-w-md"
              >
                {contentText === "확인" ? "닫기" : "취소"}
              </button>
            </div>
          </Content>
        </StyledModal>
      </Wrapper>
    </React.Fragment>
  );

  return isShown ? ReactDOM.createPortal(Modal, document.body) : null;
};
  • 근데 만들다가 보니 오류가 발생하는거 아니면 굳이 수정 컨펌을 제작할 필요가 없을것 같았다.

  • 왜냐하면 수정페이지를 따로 만들지 않고 수정-수정완료 토글로 새로고침이나 페이지 이동 없이 처리되고 있기 때문.

  • 오히려 사용자경험이 안좋아질 것 같아서 삭제, 오류, 가입확인 모달까지만 제작하기로 했다.

  • 어떤 형태의 에러라도 들어와야 하니까 any를 쓰고, 그 중에서 axios Error일때만 메시지를 보여준다.

  • 오류가 없을때는 준비된 메시지를 뱉는데 저거 두 줄을 위해 컴포넌트를 분리하기엔 작은 프로젝트라서 그냥 저기에 로그인과 회원가입의 경우를 고정으로 집어넣어주었다.

import React from "react";
import { AxiosError } from "axios";

interface LabelProps {
  content?: any;
  confirm?: boolean;
}

const ModalContent = (content: LabelProps, confirm: boolean) => {
  const message = confirm
    ? "로그인에 성공했습니다."
    : "회원가입에 성공했습니다. 자동으로 로그인합니다.";
  const [errorMessage, setErrorMessage] = React.useState<any>();

  React.useEffect(() => {
    content.content instanceof AxiosError &&
      setErrorMessage(content.content?.response?.data);
  }, []);

  return (
    <div className="h-32 flex justify-center items-center">
      {errorMessage?.details || message}
    </div>
  );
};

export default ModalContent;

존재하는 alert 모두 모달로 교체(메인스레드가 멈추게 하는 alert은 쓰지 않는게 좋음)

  • 서치를 통해 프로젝트 내에 있는 alert을 모두 찾아 바꾸어 주었다.

3. 방어적 버튼 : 사용자가 누를만한 위치에 있는 버튼에 중요한 기능을 달지 않기

파괴적 버튼 : 삭제같은걸 하는 버튼에는 꼭 해당 동작의 중요도를 선명하게 알려줄 수 있는 색상의 버튼을 배치하자

방어적 버튼과 파괴적 버튼 css

  • 삭제모달의 경우 삭제버튼 호버시 백그라운드와 텍스트가 붉은색으로 변한다!
  • 방어적 버튼 : 닫히고 열리는 사이드바에 2개 기능이 들어있으니 모바일 페이지에서 잘못누르기 쉬운 것 같았다.
<el.HiddenBox close={close}>
  <button
type="button"
className="absolute bottom-1 left-2 md:hidden 
hover:text-yellow-300 dark:text-white font-bold"
onClick={() => setOpenCreateToDo((state) => !state)}
  >
    {openCreateToDo ? "make to Do" : "detail to Do"}
</button>
<div className="flex flex-row w-full">
  {matches && (
   <>
   <CreateToDoForm />
   <DetailPage />
   </>
   )}
   {!matches && !openCreateToDo && <DetailPage />}
   {!matches && openCreateToDo && <CreateToDoForm />}
     </div>

    <button
    className="mt-5 -mb-10 flex justify-center items-center mx-auto w-8 
    hover:fill-yellow-300 active:fill-yellow-500 active:scale-110"
    onClick={() =>
    window.scrollY > 120 && setClose((state) => !state)
   }
    >
      <Arrow />
      </button>
    </el.HiddenBox>
  • 이렇게 사이드바에 유즈미디어쿼리 훅을 적용해서 width를 100%로 잡고 to do버튼을 누르면 각 기능이 토글되게 했다.

image

  • 그리고 물론 방어적 버튼의 규칙 - 우측 상단에 중요한버튼 놓지 않기!
    를 적용해서 좌측 상단에다가 배치했다.
  • 약간 버튼스럽게 바꾸고 텍스트레이어 크기를 키우기..!

image

4. onbeforeunload : 사용자가 뒤로가기나 새로고침을 했을때 한번 붙잡아주는 기능!

  • 예전에 노마드코더에서 들었던 실전형 리액트 훅 10개에 있던 것이 생각났다. <-- 이강의 좋다 조금 옛날강의라 그대로 하면 오류가 좀 나는게 단점이지만..

  • 분명히 타입스크립트 버전으로 누군가 만든게 있을것 같다는 강력한 느낌이 와서 검색해보았다.

  • 있었다. 그래서 이건 쉬웠다.

https://sooya14.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8-Hooks-%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0-2-useEffect-useTitle-useClick-useConfirm-usePreventLeave

  • 실제로 작동하는지 테스트를 돌리고 버튼없이도 돌아가게끔 적당히 고쳐서 Home에 적용시켜주었다.

5. 스켈레톤 : 스켈레톤을 만들자!

  • 서스펜스와 css를 활용하니 스켈레톤을 간단하게 만들 수 있었다.
  • 카드에다가 서스펜스를 걸어주고
return (
    <>
      <div className=" w-full flex flex-col">
        {data ? (
          data?.data.map((todos: ToDoProps, i: number) => (
            <div key={`${todos.id}-${i}`}>
              <React.Suspense fallback={<el.Skeleton />}>
                <Card {...todos} />
              </React.Suspense>
            </div>
          ))
        ) : (
          <div className="flex shadow-md bg-white mb-10 h-44 justify-between  flex-col md:flex-row dark:bg-gray-700 dark:text-white">
            <div className="flex justify-center items-center m-auto text-2xl ">
              아직 todo가 없습니다 얼른 만들어보세요!
            </div>
          </div>
        )}
      </div>
    </>
  );
  • 서스펜스 용으로 간단한 카드 모양을 만들어서 씌우면
import React from "react";
/** @jsxImportSource @emotion/react */
import tw, { css, styled, theme } from "twin.macro";

export const Skeleton = () => {
  return (
    <>
      <div className="flex shadow-md bg-white mb-10 h-44 flex-col md:flex-row dark:bg-gray-700 dark:text-white animate-pulse">
        <div className="animate-pulse">
          <div className="w-3/5 bg-gray-400 h-10 m-5 mt-6 opacity-70"></div>
          <div className="w-2/5 bg-gray-400 h-7 ml-5 -mt-1 opacity-60"></div>
          <div className="w-2/5 bg-gray-400 h-7 ml-5 mt-3 opacity-60"></div>
        </div>
        <div className="absolute right-10 animate-pulse">
          <div className="w-14 bg-gray-400 h-10 mr-5 mt-5 opacity-70"></div>
          <div className="w-14 bg-gray-400 h-10 mr-5 mt-2 opacity-70"></div>
          <div className="w-14 bg-gray-400 h-10 mr-5 mt-2 opacity-70"></div>
        </div>
      </div>
    </>
  );
};

export default Skeleton;

image

  • 얼른 gif 찍는거 찾아봐야겠다. 이거만 다하고
  • 어쨌든 다크모드와 모바일도 적용시켰다!

image

6. 기타

  • 콘솔로그 모두 제거
  • 이클립스 다양한 환경에서 테스트 & 확인
  • 카드가 1개도 없을때의 예외처리

이제 남은거

  • 리드미 (gif하기) && JSDoc 어노테이션 공부
  • 코드 개선에 대해 생각하기
  • 인성면접 준비하기
profile
It's an adventure time!

0개의 댓글

관련 채용 정보