[15주차 Day5] 스프린트 3: React(TypeScript) 기반의 동적 UI 개발

반 히·2024년 6월 11일

데브코스

목록 보기
43/58
post-thumbnail

📚 Part 12 리팩토링


📁 중간 회고 도출 아이템

  1. alias 적용 (절대 경로 사용을 위해)
  2. 중복코드 제거
  3. 스니펫 만들기
  4. useAuth 훅 만들기 (login, signup 등 이동시키기)
  5. react-query 적용 (서버 상태를 관리하는 라이브러리)
  6. 다양한 UI 경험

📌 import alias

CRACO를 이용할 것임 (환경들을 변경하는 작업)

npm i -D @craco/craco
npm install -D craco-alias

터미널에 위 명령을 적어 설치해줌

{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        }
    }
}

tsconfig.paths.json 파일 생성

const cracoAlias = require('craco-alias');

module.exports = {
    plugins: [
        {
            plugin: cracoAlias,
            options: {
                source: 'tsconfig',
                baseUrl: '.',
                tsConfigPath: 'tsconfig.paths.json',
                debug: false,
            }
        }
    ]
}

craco.config.js 파일 생성

"extends": "./tsconfig.paths.json",
"include": ["src", "craco.config.js"]

tsconfig.json에 다음 내용 추가

import { Book } from '../../models/book.model';
import { getImgSrc } from '../../utils/image';
import { formatNumber } from '../../utils/format';

// 위와 같은 경로를 아래처럼 수정할 수 있다 

import { Book } from '@/models/book.model';
import { getImgSrc } from '@/utils/image';
import { formatNumber } from '@/utils/format';

📌 중복 제거

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

App.tsx 파일의 라우터 설정의 중복 코드를 수정한다.

// 공통 요청 부분
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;
};

http.ts의 코드를 수정하여 api에서의 중복 코드를 공통 모듈로 만든다

📌 Snippet 만들기

React와 TypeScript를 사용하여 프로젝트를 개발할 때, 반복적으로 작성해야 하는 코드 패턴이 많다. 이러한 반복 작업을 줄이고, 생산성을 높이기 위해 코드 스니펫을 사용하는 것이 매우 유용하다.

위와 같은 extension을 설치하여 더욱 편리하게 스니펫 생성이 가능하다. $1, $2 등으로 키보드 입력 순서를 결정할 수 있다.

🔗 파일 이름을 기반으로 하는 React 컴포넌트 파일 생성 스니펫

{
	"Component Base": {
		"prefix": "comp_base",
		"body": [
			"import { styled } from 'styled-components';",
			"",
			"function ${TM_FILENAME_BASE}() {",
			"   return (",
			"    <${TM_FILENAME_BASE}Style></${TM_FILENAME_BASE}Style>",
			"   );",
			"}",
			"",
			"const ${TM_FILENAME_BASE}Style = styled.div`",
			"`;",
			"",
			"export default ${TM_FILENAME_BASE}"
		]
	}
}

🔗 커스텀 컴포넌트 스니펫

{
	"_comp": {
			"prefix": "_comp",
			"body": "import { styled } from 'styled-components';\r\n\r\nfunction $1() {\r\n    return (\r\n    <$1Style></$1Style>\r\n    );\r\n}\r\n\r\nconst $1Style = styled.div`\r\n`;\r\n\r\nexport default $1;",
			"description": "컴포넌트 작성"
		}
}

🔗 테마 값 스니펫

{
	"_theme_val": {
		"prefix": "_theme_val",
		"body": "${({theme}) => theme.$1};",
		"description": "테마 값 참조"
	}
}

🔗 useEffect 스니펫

{
	"_effect": {
		"prefix": "_effect",
		"body": "useEffect(() => {\r\n        $2\r\n    }, [$1]);",
		"description": "useEffect 훅"
	}
}

🔗 Title component 스니펫

{
	"_title": {
		"prefix": "_title",
		"body": "<Title size='$1' color='$2'>\r\n    $3\r\n</Title>",
		"description": "타이틀 컴포넌트"
	}
}

🔗 CSS Flex 레이아웃 스니펫

{
	"_flex": {
		"prefix": "_flex",
		"body": "display: flex;\r\nalign-items: start;\r\njustify-content: space-between;\r\ngap: $1;",
		"description": "CSS Flex 레이아웃"
	}
}

📌 useAuth 훅 만들기

import { login, resetPassword, resetRequest, signup } from "@/api/auth.api";
import { LoginProps } from "@/pages/Login";
import { useAuthStore } from "@/store/authStore";
import { useAlert } from "./useAlert";
import { useNavigate } from "react-router-dom";
import { SignupProps } from "@/pages/Signup";
import { useState } from "react";

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

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

    // 메소드
    const userLogin = (data: LoginProps) => {
        login(data).then((res) => {
            // 상태 변화 
            storeLogin(res.token);
            console.log(res.token);
            showAlert("로그인 완료되었습니다.");
            navigate('/');
        }, (error) => {
            showAlert('로그인이 실패했습니다.');
        });
    };

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

    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
    };
};

📌 react-query

react-query는 굉장히 많이 사용되고 있는 서버 state 라이브러리임.

npm i react-query --save

react-query의 장점은 자동으로 데이터를 동기화해주는 역할을 한다는 것이다. 화면에 대한 부가적인 기능도 지원하며, 서버와의 요청값을 요청하는 cost를 아낄 수 있다. 선언적 데이터는 물론이고 로딩 상태나, 에러 상태, 에러 핸들링을 기본적으로 제공하여 화면 렌더값을 만들기에 편리하다. 개발자로서 생산성이 향상되며 코드의 길이도 짧아진다. 서비스 전반적으로 fetch나 변화 부분을 react-query로 관리하면 일관된 구조를 지킬 수 있어서 데이터의 흐름을 통제하는 부분에 있어서 굉장히 유용한 도구이다.

// queryClient.ts
import { QueryClient } from "react-query";

export const queryClient = new QueryClient();
// App. tsx
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <BookStoreThemeProvider>
        <RouterProvider router={router} />
      </BookStoreThemeProvider>
    </QueryClientProvider>
  );
}
// useBooks.ts 
import { useLocation } from "react-router-dom";
import { fetchBooks } from "../api/books.api";
import { QUERYSTRING } from "../constants/querystring";
import { LIMIT } from "../constants/pagination";
import { useQuery } from "react-query";

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,
            newBook: 
                params.get(QUERYSTRING.NEWS) ? 
                true : undefined,
            currentPage: 
                params.get(QUERYSTRING.PAGE) ? 
                Number(params.get(QUERYSTRING.PAGE)) : 1,
            limit: LIMIT
        })
    );

    return { 
         books: booksData?.books,
         pagination: booksData?.pagination,
         isEmpty: booksData?.books?.length === 0,
         isBooksLoading
     };
};

0개의 댓글