Next.js 13과 redux/toolkit, styled-components 등으로 북마크 기능이 들어있는 쇼핑몰 앱을 구현한 프로젝트이며, 설명에 필요한 코드만 최소한으로 서술되었습니다. :
(1) gitHub - README 및 전체 코드 보러 가기
(2) 프로젝트 요구 명세서 보기 (해당 프로젝트 첫 포스팅)
처음으로 Redux Toolkit을 적용시켜본 프로젝트인 만큼 시간 소요도 상당했고, 그냥 기존의 익숙한 Redux를 쓸까 고민도 많이 했지만 결과적으로는 성공적으로 구현을 했다..! 이번 포스팅에서는 프로젝트에서 Redux Toolkit을 사용하여 전역으로 데이터를 관리한 방법과 추가 기능인 Redux Toolkit Query를 이용하여 데이터를 불러오는 방법까지 정리해보았다.
Redux Toolkit(이하 RTK) 이란?
RTK는 Redux의 개발 경험을 향상시켜 주는 공식적인 Redux 라이브러리이다.
- RTK는 Redux 애플리케이션을 설정하기 위한 보일러플레이트 코드를 크게 줄여준다.
- 예를 들어 기존의 Redux를 쓸 때 작성해야 했던 Action, ActionType은 RTK에서는 createSlice에서 추출할 수 있기 때문에 직접 작성할 필요가 없다.
- 이 외에도 많은 편의점이 있다. : 해당 블로그 글 참고
- RTK는 Redux Thunk를 내장하고 있어 비동기 작업을 처리하기 위한 미들웨어를 쉽게 사용할 수 있다.
- Redux Thunk는 액션 생성자에서 비동기 작업을 처리하고 상태를 업데이트하는 로직을 구현할 때 사용할 수 있다.
- 기존 Redux에서는 전역에서 자주 사용되는 api를 호출하거나, api 호출한 결과를 여러 군데에서 사용해야 할 상황이 생기는데 이러한 비동기 처리를 해주기 위해서는 따로 redux-thunk나 redux-saga와 같은 미들웨어를 설치해서 사용해야만 했다.
- 개발 도구인 Redux DevTools를 사용할 때 기본적인 설정을 제공하여 디버깅과 상태 추적을 용이하게 한다.
- RTK에서는 따로 redux-devtools-extension을 설치하지 않아도 된다.
- 아래와 같이 코드 한 줄로 개발 도구를 설정하는 과정을 단순화시켜 애플리케이션을 더 쉽게 디버깅할 수 있게 해준다.
const store = configureStore({
reducer: {
user: userReducer,
},
devTools: process.env.NODE_ENV !== 'production',
});
🟡 RTK Query란?
RTK에는 createAsyncThunk를 사용하여 기존 Redux의 thunk처럼 async 요청을 관리할 수 있는 기능이 있다.
- createAsyncThunk는 async 요청의 응답을 createSlice를 활용해서 코드를 작성해서 관리해줘야 하는 번거로움이 있다.
export const fetchSlice = createSlice({
name: 'posts',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUser.pending, (state) => {
state.loading = 'pending';
});
builder.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.user = action.payload;
});
builder.addCase(fetchUser.rejected, (state) => {
state.loading = 'failed';
});
},
});
RTK Query는 createSlice와 createAsyncThunk의 기능을 동시에 사용하여 react-query와 유사하게 백엔드에서 넘어온 데이터를 관리할 수 있는 기능이다.
- 캐싱 관련 기능도 지원하므로 불필요한 요청을 줄일 수 있다.
- post 하자마자 자동으로 get 하는 기능도 구현 가능하다.
- 프론트 데이터(createSlice)와 api 데이터를 관리(RTK Query)하는 로직 자체를 분리시킬 수도 있다.
- 아래부터는 RTK Query 관련 글이나 코드 설명에는 필요한 경우에, 🟡 아이콘을 넣어 구분해줄 예정이다.
createAsyncThunk와 createApi ?
RTK에는 데이터 가져오기와 상태 업데이트를 관리하기 위해 createAsyncThunk와 createApi라는 두 가지 주요 유틸리티가 있다.
- createAsyncThunk는 비동기 작업을 수행하는 Redux 액션 생성자를 생성하는 데 사용된다.
- 일반적으로 네트워크 요청을 수행하고 해당 요청에 대한 응답을 처리하는 데 사용된다.
- 이 유틸리티는 액션 생성자를 정의할 때 비동기 작업의 세 가지 상태 (시작, 성공, 실패)를 처리하기 위한 간단한 패턴을 제공한다.
- createAsyncThunk를 사용하면 액션 생성자에 대한 비동기 작업 로직을 명확하게 정의할 수 있으며, Redux Thunk 미들웨어와 함께 사용된다.
- createApi는 일반적인 CRUD (Create, Read, Update, Delete) 작업을 수행하는 RESTful API와 상호 작용하기 위한 유틸리티이다.
- API 요청과 관련된 상태를 자동으로 관리할 수 있으며, 액션 생성자 및 리듀서를 생성하는 데 도움이 된다.
- createApi는 Axios나 Fetch와 같은 네트워크 라이브러리와 통합되며, 기본적인 RESTful 엔드포인트에 대한 CRUD 작업을 자동으로 처리할 수 있다.
RTK 로 구현한 기능
- 북마크 기능 ( 다음 포스팅에서 )
- 북마크에 추가되는 아이템 / 삭제된 아이템 데이터 전역에서 관리
- (메인 페이지, 상품 페이지, 북마크 페이지, 모달창에서 사용)
- 토스트 알림 기능 ( 다음 포스팅에서 )
- (🟡 Query) api 호출하고, 해당 데이터 받아오기
- 전체 아이템 데이터(100개)를 받아오는 쿼리
- 특정한 숫자(변수는 counter)에 해당되는 개수의 아이템 데이터만 받아오는 쿼리
RTK 스토어 설정하기
설명은 코드 안에 넣어두었다.
// RTK에서 제공하는 Redux store을 설정하는 함수 (여러가지 옵션을 받아 store을 구성한다.)
import { configureStore } from "@reduxjs/toolkit";
// 🟡 RTK query와 북마크, 토스트 리듀서
import { productApi } from "./productApi";
import bookMarkedProducts from "./bookmarkReducer";
import toastReducer from "./ToastReducer";
export const store = configureStore({
// store의 상태를 업데이트하는 데 사용되는 리듀서 함수들을 정의한다.
reducer: {
bookMarkedProducts,
toastReducer,
// productApi.reducerPath는 productApi로 생성된 API slice에서 사용되는 reducer의 경로
// 🟣 설명(2) : API slice란? 2번 챕터에서 설명
[productApi.reducerPath]: productApi.reducer,
},
// 개발 환경에서만 Redux DevTools를 활성화
devTools: process.env.NODE_ENV !== "production",
// Redux 미들웨어를 설정한다.
// productApi와 통합된 미들웨어를 추가하여 API 요청, 응답을 처리하고 상태 업데이트를 관리할 수 있게 된다.
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat([productApi.middleware]),
});
// 타입 정의
// Redux store의 전체 상태 타입과 dispatch 함수 타입을 나타낸다.
// 이를 통해 타입 안정성을 확보하고 Redux 액션과 상태를 다룰 때 타입 검사를 할 수 있다. // 🟣 설명(1)
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
🟣 설명(1) ReturnType?
store.getState() 함수는 Redux store에서 현재 상태를 반환한다.
- typeof store.getState() 는 해당 반환 값을 추론하여 타입으로 사용하는 것이다.
- ReturnType은 함수의 반환 타입을 가져오는 TypeScript 유틸리티 타입이다.
- 따라서 RootState는 Redux store의 타입이 된다.
- 예를 들어, Redux store의 bookMarkedProducts 상태를 참조하고 싶다면 RootState['bookMarkedProducts']와 같이 접근하여 타입 검사를 수행할 수 있다.
typeof store.dispatch 역시 액션을 디스패치하는 해당 함수의 타입을 추론하여 사용하는 것을 의미한다.
- 예를 들어, AppDispatch를 사용하여 액션을 디스패치하려면 아래와 같이 사용할 수 잇다.
- dispatch는 AppDispatch 타입으로 정의되어 Redux 액션을 디스패치할 수 있다.
.
.
import { AppDispatch } from './types';
const dispatch: AppDispatch = store.dispatch;
// 액션 디스패치 예시
dispatch({ type: 'user/login', payload: { email: 'test@example.com', password: '123456' } });
provider 넣기
Next.js는 기본 SSR을 지원하기 대문에 Provider을 넣기 위해서는 제일 상단에 "use client"를 써줘야 한다.
- 이 문구를 넣은 코드들에 한해서 CSR을 지원한다.
- 아래와 같이 provider을 작성해준 후에,
"use client";
import { store } from "./store";
import { Provider } from "react-redux";
// Providers 함수 컴포넌트는 { children: React.ReactNode } 타입의 매개변수를 받으며,
// children은 리액트 컴포넌트 트리에서 Providers 컴포넌트의 하위요소 즉, JSX로 표현된 자식 요소이다.
export function Providers({ children }: { children: React.ReactNode }) {
// Provider 컴포넌트는 Redux store를 하위 컴포넌트에게 전달하여 Redux 상태를 사용할 수 있게 해준다.
return <Provider store={store}>{children}</Provider>;
}
최상위 컴포넌트에 import하여 넣어주면 된다.
.
.
import { Providers } from "./redux/provider";
.
.
export default function RootLayout({ children }) {
return (
<html lang="ko" className="noScroll">
<Providers>
<body>
<Header />
<main>{children}</main>
<NotificationCenter />
<Footer />
</body>
</Providers>
</html>
);
}
🟡 1. RTK Query 설정하기
아래와 같이 RTK Query를 이용하여 API와 상호작용하기 위한 productApi를 생성했다.
// createApi 함수는 Redux Toolkit Query에서 제공하는 함수로, API slice를 생성하는 데 사용된다. 🟣 설명(2)
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
// createApi 함수에는 몇 가지 옵션을 제공하여 API 설정을 구성한다.
export const productApi = createApi({
// 생성되는 API slice의 이름
reducerPath: "productApi",
// 페이지가 다시 포커스를 받을 때 API를 재요청할지 여부를 설정
refetchOnFocus: true,
// fetchBaseQuery는 기본적인 네트워크 요청을 수행하는 함수
// baseUrl과 함께 사용되어 기본 API 엔드포인트를 정의하고 HTTP 요청을 보내는 데 사용됨
// 기본적으로 fetch 함수를 사용하여 HTTP 요청을 수행하며, Promise를 반환한다.
baseQuery: fetchBaseQuery({
baseUrl: "http://cozshopping.codestates-seb.link/api/v1/",
}),
// 실제 API 호출을 정의하는 부분으로 builder.query 함수를 사용하여 각각의 API 호출을 설정
endpoints: (builder) => ({
getProducts: builder.query({
query: () => "products",
}),
getProductByCount: builder.query({
query: (counter) => `products?count=${counter}`,
}),
}),
});
// productApi에서 생성된 API 호출에 대한 React 커스텀 훅들
// 이를 통해 컴포넌트에서 해당 API 호출을 손쉽게 사용할 수 있다.
// useGetProductsQuery 훅은 getProducts API 호출을 수행하고 해당 결과를 가져오는 데 사용된다.
export const { useGetProductsQuery, useGetProductByCountQuery } = productApi;
🟣 설명(2) API slice란?
API slice는 RTK에서 제공하는, RESTful API와의 상호 작용을 관리하기 위한 기능이다.
- API 호출과 관련된 상태와 로직을 중앙 집중화하여 관리할 수 있다.
- API slice를 생성할 때, 일반적으로 다음과 같은 단계를 따른다.
- API 호출을 정의하는 endpoints 객체를 생성한다. 이 객체에는 각각의 API 호출에 대한 정보와 설정인 API의 URL, HTTP 메서드, 요청 및 응답에 대한 변형 로직 등이 포함될 수 있다.
- createAsyncThunk로 비동기 액션 생성자를 생성하여 endpoints 객체에서 정의한 API 호출을 수행하고, 해당 호출에 대한 요청 및 응답 처리를 담당한다.
- createSlice를 사용하여 API slice를 생성한다. API 호출에 대한 상태 (로딩 중, 성공, 실패 등)를 관리하기 위한 액션 및 리듀서 코드가 자동으로 생성된다.
- createApi를 사용하여 API slice를 한 곳에 통합한다. 이를 통해 API slice에서 생성한 액션 및 리듀서를 Redux store에 등록하고, API 호출 관련 미들웨어를 설정할 수 있다.
- API slice를 사용하면 애플리케이션에서 API 호출과 관련된 로직을 간단하게 구현할 수 있으며, 비동기 작업의 상태를 쉽게 관리할 수 있다.
1-2. hook 설정하기
React 애플리케이션에서 사용되는 커스텀 훅을 정의하는 코드이다.
- Redux의 dispatch 함수와 상태 선택 기능을 편리하게 활용하기 위함이다.
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";
// AppDispatch 타입을 명시하고 해당 타입을 반환하는 커스텀 훅을 생성한다.
// 이렇게 하면 컴포넌트에서 useAppDispatch를 호출하여 Redux의 dispatch 함수를 쓸수 있다.
export const useAppDispatch = () => useDispatch<AppDispatch>();
// useSelector 훅을 가져와서 Redux store의 상태를 선택하는 역할을 한다.
// RootState 타입을 명시하고 해당 타입을 반환하는 커스텀훅을 생성한다.
// 이렇게 하면 컴포넌트에서 useAppSelector을 호출하여 Redux store의 상태를 확인할 수 있다.
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
1-3. 리액트 컴포넌트에 적용하기
화면에 4개의 아이템만 불러와 렌더링하는 컴포넌트
import { useGetProductByCountQuery } from "./redux/productApi";
.
.
.
export default function MainPage() {
const { isLoading, isFetching, data, error } = useGetProductByCountQuery(4);
return (
<Container>
{error ? ( <!-- 에러가 났을 때 -->
<p>Oh no, there was an error</p>
) : isLoading || isFetching ? ( <!-- 데이터를 받아오거나 렌더링 중에 있을 때 -->
<Loading />
) : data ? ( <!--데이터를 받아왔을 때-->
<ItemContainer>
<H2>상품 리스트</H2>
<ItemsBox>
{data.map((product) => (
<MainList key={product.id} product={product} />
))}
</ItemsBox>
</ItemContainer>
) : null}
화면에 모든 데이터를 다 불러오는 컴포넌트
useGetProductsQuery를 import 해서 호출해주면 된다. 나머지는 위의 코드와 동일하다.
const { data, error, isLoading, isFetching } = useGetProductsQuery(null);
난 아직도 잊지 못한다.
제한 시간의 압박 속에서 아.. 그냥 기존의 Redux를 쓸까, 고민에
몇 십번의 실패 끝에 콘솔에 데이터가 찍혀 나온 순간을,
그 짜릿함을...!!!!
초기 UI 구현 전
데이터가..... 들어왔다....!!!! 이미지가... 데이터가 렌더링 되고 있어.
데이터가 완전히 불러와지기 전에 Loading.... 이 뜨는 모습
아... 이 맛에 코딩하나 보다.
너무 재밌다.
'📌 TOY-PROJECT > 2305 쇼핑몰 웹앱 (북마크 기능)' 카테고리의 다른 글
README 작성하기 (0) | 2023.06.07 |
---|---|
redux/toolkit(RTK) / 토스트 애니메이션 자연스럽게 구현하기 (0) | 2023.05.16 |
헤더 컴포넌트의 DropDown 메뉴 만들기 (0) | 2023.05.14 |
Next.js 13 - StackoverFlow에 질문글을 올리다. (1) | 2023.05.12 |
git 전략 : 작업 브랜치 생성 (0) | 2023.05.12 |