[프로그래머스] 프론트엔드 심화: 프로젝트(8)

Lina Hongbi Ko·2024년 11월 19일
0

Programmers_BootCamp

목록 보기
59/76
post-thumbnail

2024년 11월 19일

✏️ 중간 회고(1)

  • 회고 하는 이유

    • 성장과 학습
    • 문제 해결
    • 유연성과 적응성
    • 퍼포먼스 향상
  • 주요 학습 주제

    • 타입과 모델

      • 스타일 타입 정해줘서 구현 하면 안전 & 재사용성
      • 프로젝트 초기, theme을 이용해 타입을 작성함 → 미리 타입으로 키를 정의 해놓으면, 나중에 정의 해놓지 않은 키를 무심코 적용했을 때 타입 방어 가능
        • 유니온타입, Record<, stirng>, [key in ]: { xx: string}
      • 타입과 인터페이스 : 오류 제한 & 확장성 있는 것을 만드는데 유용
      • 타입 오버라이드
      • 데이터 모델을 인터페이스를 사용해 컴포넌트나 api fetch에 적용시킴 → 오류 제한
      • extends(확장) → 상속받은 것이 바뀌면 같이 바뀜
      • 컴포넌트의 props, ui의 기능으로써 모델 사용 (e.g- viewmode)
      • <pick<>>, <omit<>> 적절한 곳에서 잘 사용할 것
    • 데이터 흐름

      • 커스텀훅 사용해서 데이터 전달해줄 것 → 재사용성
      • 커스텀 훅을 통해 데이터를 관련 컴포넌트가 같이 공유할 수 있음
      • 낙관적 업데이트를 사용하게 할 수 있음
    • 컴포넌트 작성

      • 조건부 연산자, 삼항 연산자, map 루프문 잘 사용할 것
    • css 스타일링

      • flex, grid 숙지할 것
    • 커스텀 훅

      • use를 앞에 꼭 붙일 것
      • 별도의 상태를 가지고 어떤 기능을 한다 → 모두다 훅으로 작성할 수 있음
      • 훅을 통해 데이터 교환이나 데이터 흐름, 화면 로직 등을 공통 모듈로써 작성할 수 있음

✏️ 중간 회고 (2)

  • 생산성

    • 스니펫 사용
    • 기능 단위 작업 흐름 파악
      • 자기만의 기능 단위 작업 흐름을 파악하고 개선해 나갈 것!
      • 모델 규약은 백엔드와 미리 먼저 얘기하고 제일 먼저 정의할 것 → 프로젝트 훨씬 시작하기 쉬움
  • KPT 회고

✏️ import alias

  • 상대 경로를 이용하면 규모가 커지는 프로젝트일수록 유지보수가 힘듦 (depths가 계속 깊어짐, path를 변경한다던지 하나의 폴더로 묶을때 문제가 생길 수 있음) → 절대 경로 이용

  • CRACO (CreateReactAppConfigurationOverride) 사용

    • CreateReactApp 설정을 override함 → 그 중 하나인 alias 를 이용해 절대 경로를 사용할 것임

    • 설치

      • npm i -D @craco/craco
      • npm install -D craco-alias
    • 설정

      • tsconfig.paths.json 파일 생성

        // tsconfig.paths.json
        
        {
          "compilerOptions": {
            "baseUrl": ".",
            "paths": {
              "@/*": ["src/*"]
            }
          }
        }
      • tsconfig.json 파일 수정

        // tsconfig.paths.json
        
        {
          "compilerOptions": {
            "target": "es5",
            "lib": [
              "dom",
              "dom.iterable",
              "esnext"
            ],
            "allowJs": true,
            "skipLibCheck": true,
            "esModuleInterop": true,
            "allowSyntheticDefaultImports": true,
            "strict": true,
            "forceConsistentCasingInFileNames": true,
            "noFallthroughCasesInSwitch": true,
            "module": "esnext",
            "moduleResolution": "node",
            "resolveJsonModule": true,
            "isolatedModules": true,
            "noEmit": true,
            "jsx": "react-jsx"
          },
          "extends": "./tsconfig.paths.json",
          "include": [
            "src", "craco.config.js"
          ]
        }
      • craco alias 파일 생성

        // craco.config.js
        
        const cracoAlias = require("craco-alias");
        
        module.exports = {
          plugins: [
            {
              plugin: cracoAlias,
              options: {
                source: "tsconfig",
                baseUrl: ".",
                tsConfigPath: "tsconfig.paths.json",
                debug: false,
              },
            },
          ],
        };
      • package.json 수정

        // pacakge.json
        
        ... 생략 ...
        
        "scripts": {
            "start": "craco start",
            "build": "craco build",
            "test": "craco test",
            "eject": "react-scripts eject",
            "type-check": "tsc --noEmit --skipLibCheck"
          },
        
          ... 생략 ...
  • craco 설정 하면서 궁금했던 것

    • package.json 파일에서 scripts 부분에 script-react start, script-react build … 이렇게 설정 되어 있는데 왜 우리는 npm run start를 사용했을까 의문이 듦
  • 적용해보기

// components / books / BookItems.tsx

import styled from 'styled-components'
import { Book } from '@/models/book.model';
import { getImgSrc } from '@/utils/image';
import { formatNumber } from '@/utils/format';
import { FaHeart } from "react-icons/fa";
import { ViewMode } from '@/components/books/BooksViewSwitcher';
import { Link } from 'react-router-dom';

... 생략 ...

✏️ 중복 제거

  • 중복 코드 → 리팩토링의 대상
  • App.tsx 중복 제거
// App.tsx

... 생략 ...

const routeList = [
  {
    path: "/",
    element: <Home />
  },
  {
    path: "/books",
    element: <Books />
  },
  {
    path: "/signup",
    element: <Signup />
  },
  {
    path: "/reset",
    element: <ResetPassword />
  },
  {
    path: "/login",
    element: <Login />
  },
  {
    path: "/books/:bookId",
    element: <BookDetail />
  }
  ,{
    path: "/carts",
    element: <Cart />
  },
  {
    path: "/order",
    element: <Order />
  },
  {
    path: "/orderlist",
    element: <OrderList />
  }
];

const router = createBrowserRouter(
  routeList.map((item)=>{
    return {
      ...item,
      element: <Layout>{item.element}</Layout>,
      errorElement: <Error />
    }
  })
);

... 생략 ...
  • api 중복 제거
// api / http.ts

... 생략 ...

type RequestMethod = "get" | "post" | "put" | "delete";

export const requestHandler = async <T>(method:RequestMethod, 
url: string, payload?: T) => {
  let response;

  switch(method) {
    case "post" :
      response = await httpClient.post(url, payload);
      break;
    case "get" :
      response = await httpClient.get(url);
      break;
    case "put" :
      response = await httpClient.put(url, payload);
      break;
    case "delete" :
      response = await httpClient.delete(url);
      break;
  }

  return response.data;
}
// api / order.api.ts
import { requestHandler } from './http';

export const order = async (orderData: OrderSheet) => {
  return await requestHandler<OrderSheet>('post', '/orders', orderData);
}

export const fetchOrders = async () => {
  return await requestHandler('get', '/orders');
}

export const fetchOrder = async (orderId: number) => {
  return await requestHandler('get', '/orders/${orderId}');
}

✏️ 스니펫 만들기

  • 반복적으로 입력해야하는 것 → 스니펫 사용해서 편리하게 코드 작성
  • 설정 → snippets → typescriptreact.json → 스니펫 등록 (직접 스니펫을 등록하긴 어려움) → extension 이용
  • extension : snippet generator (fiore) 설치
import styled from "styled-components";

const $1 = () => {
	return (
		<$1Style>
			<h1>$1</h1>
		</$1Style>
	);
}

const $1Style = styled.div``;

export default $1;
  • 위 코드 드래그 + 오른쪽클릭 Generate snippet → typescriptreact → snippet name(comp) → trigger name(comp) → description(컴포넌트 작성)

✏️ useAuth 훅 만들기

  • Login 페이지에서 데이터를 가져오는 부분 커스텀훅으로 만들어서 재사용성 높이기
// hooks / useAuth.ts

import { LoginProps } from '@/pages/Login';
import { useAuthStore } from '../store/authStore';
import { useNavigate } from 'react-router-dom';
import { useAlert } from './useAlert';
import { login } from '@/api/auth.api';

export const useAuth = () => {
  const navigate = useNavigate();
  const {showAlert} = useAlert();

  // 상태
  const { storeLogin, storeLogout, isloggedIn } = useAuthStore();

  // 메소드
  const userLogin = (data:LoginProps) => {
    login(data).then((res) => {
      
      // 상태 변화
      storeLogin(res.token);
      showAlert("로그인 완료되었습니다.");
      // window.location.href = "/";
      navigate("/");
    }, (error) => {
      showAlert("로그인이 실패했습니다.");
    });
  }
  // 리턴
  return { userLogin }
}
// pages / Login.tsx

... 생략 ...


export interface LoginProps {
  email : string;
  password : string;
}

const Login = () => {

  const { userLogin } = useAuth();
  
  ... 생략 ...
  
  const onSubmit = (data: LoginProps) => {
    userLogin(data);
  };

  return (
    <>
      <Title size="large">로그인</Title>
      <SignupStyle>
        <form onSubmit={handleSubmit(onSubmit)}>
        
        ... 생략 ...
  • Signup 페이지에서 데이터를 가져오는 부분 커스텀훅으로 만들어서 재사용성 높이기
// hooks / useAuth.tsx

... 생략 ...

	const userSignup = (data:SignupProps) => {
	    signUp(data).then((res) => {
	      // 성공
	      showAlert('회원 가입이 완료되었습니다.');
	      navigate("/login");
	    })
	  }
  

  // 리턴
  return { userLogin, userSignup }
}
// pages / Signup.tsx

... 생략 ...

const Signup = () => {
  const { userSignup } = useAuth();

  const { register, handleSubmit, formState: {errors} } = useForm<SignupProps>();

  const onSubmit = (data: SignupProps) => {
    userSignup(data);
  };
  
  ... 생략 ...
  • ResetPassword & RequestPassword 페이지에서 데이터를 가져오는 부분 커스텀훅으로 만들어서 재사용성 높이기
// hooks / useAuth.ts

... 생략 ...

	const userResetPassword = (data:SignupProps) => {
    resetPassword(data).then(() => {
      showAlert("비밀번호가 초기화되었습니다.");
      navigate('/login');
    });
  }

  const [resetRequested, setResetRequested] = useState(false);

  const userResetRequest = (data:SignupProps) => {
    resetRequest(data).then(() => {
      setResetRequested(true);
    });
  }

  // 리턴
  return { userLogin, userSignup, userResetPassword, userResetRequest, resetRequested }
}
// pages / ResetPassword.tsx

... 생략 ...

const ResetPassword = () => {
  const { userResetPassword, userResetRequest, resetRequested } = useAuth();
  
  ... 생략 ...
  
  const onSubmit = (data: SignupProps) => {
    // if(resetRequested) {
    //   userResetPassword(data);
    // } else {
    //   userResetRequest(data);
    // }

    resetRequested ? userResetPassword(data) : userResetRequest(data);
  };
  
  ... 생략 ...

✏️ react-query (1)

  • eact-query: 서버의 state 라이브러리

  • 자동으로 데이터를 동기화 해주는 역할을 함

    • useState나 useEffect를 통해서 fetch한 내용을 잘 업데이트 했지만, 더욱 발전해서 자동으로 데이터를 동기화시켜 관리해줌
    • 일정 부분 캐싱하고, 캐싱 부유화방법도 제공 → 서버에 요청하는 값(cost)을 아낄 수 있음
    • 선언적 데이터, 로딩상태, 에러 상태, 에러 핸들링을 기본적으로 제공 → 화면 렌더를 만들기 쉬움
    • 페이지네이션과 같은 부과 기능도 제공
    • useState와 useEffect로 나눠서 관리하는 것들을 하나로 통합해서 관리 할 수 있음 → 생산성 향상
    • 서비스 전반적으로 fetch나 변화(mutation)를 가지고 관리하면 일관된 구조를 지킬 수 있음 → 데이터 흐름을 통제할 수 있음
    • https://velog.io/@kandy1002/React-Query-푹-찍어먹기 → 설명 자료 머리속에 박아야함.
  • 설치

    • npm i react-query
  • 사용

    1. queryClient 작성

      // api / queryClient.ts
      
      import { QueryClient } from 'react-query';
      
      export const queryClient = new QueryClient();
    2. queryClientProvider 넣어주기

      // App.tsx
      
      ... 생략 ...
      
      import { QueryClientProvider } from 'react-query';
      import { queryClient } from './api/queryClient';
      
      ... 생략 ...
      function App() {
      
        return(
          <QueryClientProvider client={queryClient}>
            <BookStoreThemeProvider>
              <ThemeSwitcher />
              <RouterProvider router={router} />
              </BookStoreThemeProvider>
          </QueryClientProvider>
        );
      }
      
      export default App;
    3. react-query 사용해서 데이터 사용하기

      // hooks / useBooks.ts
      
      ... 생략 ...
      
      export const useBooks = () => {
        const location = useLocation();
        const params = new URLSearchParams(location.search);
      
        const { data: booksData, isLoading: isBooksLoading } = useQuery(["books", location.search], () =>
          fetchBooks({
            category_id: params.get(QUERYSTRING.CATEGORY_ID) ? Number(params.get(QUERYSTRING.CATEGORY_ID)) : undefined,
            news: params.get(QUERYSTRING.NEWS) ? true : undefined,
            currentPage: params.get(QUERYSTRING.PAGE) ? Number(params.get(QUERYSTRING.PAGE)) : 1,
            limit: LIMIT,
          })
        );
      
            ... 생략 ...
      
          return {
          books: booksData?.books, 
          // 받아오는 데이터가 undefined일 수 있기 때문에 옵셔널 체이닝으로 써줘야함
          pagination: booksData?.pagination,
          isEmpty: booksData?.books.length === 0,
          isBooksLoading
        } ;
      };
      // pages / Books.tsx
      
      ... 생략 ...
      
      const Books = () => {
        const { books, pagination, isEmpty, isBooksLoading } = useBooks();
      
        console.log(isBooksLoading);
      
        return (
          <>
          <Title size="large">도서 검색 결과</Title>
          <BooksStyle>
            <div className="filter">
              <BooksFilter />
              <BooksViewSwitcher />
            </div>
            { !isEmpty && books && <BooksList books={books} /> }
            // books가 undfined인 경우 방어
            { isEmpty && <BooksEmpty /> }
            { !isEmpty && pagination && <Pagination pagination={pagination} /> }
          </BooksStyle>
          </>
        )
      }
      
      ... 생략 ...
  • 옵셔널 체이닝(?.), 널 병합 연산자 (??)까먹지 말기 : https://velog.io/@wlwl99/JavaScript-널-병합-연산자-옵셔널-체이닝

✏️ react-query (2)

  • loading indicator 만들기
// components / common / Loading.tsx

import styled from 'styled-components';
import { FaSpinner } from 'react-icons/fa';

const Loading = () => {
  return (
    <LoadingStyle>
      <FaSpinner />
    </LoadingStyle>
  );
}

const LoadingStyle = styled.div`
  padding: 40px 0;
  text-align: center;

  @keyframes rotate {
    100% {
      transform: rotate(360deg);
    }
  }

  svg {
    width: 70px;
    height: 70px;
    fill: #ccc;
    animation: rotate 1s linear infinite;
  }
`;

export default Loading;
// pages / Books.tsx

... 생략 ...

const Books = () => {
  const { books, pagination, isEmpty, isBooksLoading } = useBooks();

  return (
    <>
    <Title size="large">도서 검색 결과</Title>
    <BooksStyle>
      <div className="filter">
        <BooksFilter />
        <BooksViewSwitcher />
      </div>
      { isBooksLoading && <Loading /> }
      { !isEmpty && books && <BooksList books={books} /> }
      { isEmpty && <BooksEmpty /> }
      { !isEmpty && pagination && <Pagination pagination={pagination} /> }
    </BooksStyle>
    </>
  )
}

... 생략 ...
  • 반복되는 코드부분 리팩토링
    • Books 처럼 페이지 컴포넌트를 작성할 때는 조건들이 많이 들어가게 됨
      • 렌더를 복잡하게 하면 가독성이 떨어지므로 early return을 사용해야함
      • early return : return을 미리 해줌
      • 상단에서 어떤 조건으로 return해준다면, 하단의 return을 하지 않도록 하기
// pages / Books.tsx

... 생략 ...

const Books = () => {
  const { books, pagination, isEmpty, isBooksLoading } = useBooks();

  if (isEmpty) {
    return <BooksEmpty />
  }

  if (!books || !pagination || isBooksLoading) {
    return <Loading />
  }

  return (
    <>
    <Title size="large">도서 검색 결과</Title>
    <BooksStyle>
      <div className="filter">
        <BooksFilter />
        <BooksViewSwitcher />
      </div>
      <BooksList books={books} />
      <Pagination pagination={pagination} />
    </BooksStyle>
    </>
  )
}

... 생략 ...
profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글