
Redux
자바스크립트로 구동되는 어플리케이션에서 예측 가능한 상태관리를 도와주는 상태관리 라이브러리
State
state는 크게 3가지로 구분할 수 있다.
| 구분 | 설명 | 방법 |
|---|---|---|
| Local State | 하나의 컴포넌트에 영향을 미치는 상태 | useState(), useReducer() |
| Cross-Component State | 여러개의 컴포넌트에 영향을 미치는 상태 | prop chains |
| Would Wide State | 전체, 대부분의 컴포넌트에 영향을 미치는 상태 ex) authentication status | prop chains |
2, 3에 해당하는 경우 React Context, Redux를 사용해 해결한다.
Context를 사용해 2,3번을 해결할 수 있지만 크게 2가지 단점이 존재
1. Complex Setup / Management
- 여러개의 ContextProvider컴포넌트가 생기면서 관리의 어려움
- 여러개의 Provider Component를 생성해 복잡한 JSX구문
2. Performance
장점
- 상태를 예측 가능하게 만듬
- 테스트나 디버깅에 용이
특징
-
Single source of truth
애플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장
-
State is read-only
상태는 읽기전용(불변) 데이터이며, 오직 액션만 상태를 변경할 수 있다
-
변화를 일으키는 함수, 리듀서는 순수 함수여야 한다
이전 상태와 액션을 받아 다음 상태를 반환하는 순수 함수
순수함수: 반환(reture)값이 전달 인자(argument) 값에만 의존하는 함수
=> 순수 함수, 사이드 이펙트 x, 비동기 코드x
핵심 개념
Flux패턴
단방향 데이터 흐름의 디자인 패턴
Action -> Dispatch -> Store -> View(Component)
Component에서 Dispatch함수를 통해 Action 발생
-> reducer함수에 정의된 로직에 따라 store에 state를 변경 후 view에 반영
=> 애플리케이션의 구조와 상태를 심플하게 파악 가능
1. State
Redux API에서는 보통 저장소에 의해 관리되고 getState()에 의해 반환되는 하나의 상태값
2. Store
애플리케이션의 상태 값들을 내장하고 있다 (상태가 관리되는 하나의 공간)
3. Action
태를 변화시키려는 의도를 표현하는 객체
어떤 형태의 액션이 행해질지 표시하는 type 필드를 가져야 한다
4. Reducer
상태를 변화시키는 로직이 담긴 함수. 현재 상태와 Action을 이용해 다음 상태를 만들어 낸다
5. Dispatch
액션이나 비동기 액션을 받는 함수
react-redux 사용 방법
- Action 선언 및 Reducer 함수 작성
export const INCREMENT = 'increment';
export const INCREASE = 'increase';
export const DECREMENT = 'decrement';
export const TOGGLE = 'toggle';
const initialState = { counter: 0, showCounter: true };
const counterReducer = (state = initialState, action) => {
if (action.type === 'increment') {
return {
...state,
counter: state.counter + 1,
};
}
if (action.type === 'increase') {
return {
...state,
counter: state.counter + action.amount,
};
}
if (action.type === 'decrement') {
return {
...state,
counter: state.counter - 1,
};
}
if (action.type === 'toggle') {
return {
...state,
showCounter: !state.showCounter,
};
}
return state;
};
const store = createStore(counterReducer);
- Provider 컴포넌트를 사용해 store 연동
root.render(
<Provider store={store}>
<App />
</Provider>,
);
- Store내부의 State사용을 위한 useSelector와 액션을 실행 시킬 useDispatch 사용
- Component에 이벤트 - 함수 트리거
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const incrementHandler = () => {
dispatch({ type: 'increment' });
};
...
return (
...
<button onClick={incrementHandler}>Increment</button>
...
);
}
- reducer함수에서 반환값은 항상 새로운 객체를 반환 (기존 객체 변경x)
useDispatch()
액션을 호출하는 함수
useDispatch객체를 dispatch로 선언 후, dispatch 변수를 활용하여 import로 가져온 액션 호출
// store/index.js
export const itemActions = itemSlice.actions;
// item.js
import { itemActions } from '../../store/cart-slice';
const item = (props) => {
const dispatch = useDispatch();
...
const addItemHandler = () => {
dispatch(
itemActions.addItemToCart({
id,
title,
price
})
)
}
}
useSelector()
tore state에 데이터를 등록한 상태를 가져오기 위한 함수
const user = useSelector(state => state.user)
비동기를 지원하는 Redux-Saga, React-query (추가로 정리 할 예정)
강의에선 redux-toolkit을 사용하고 비동기 처리를 위해 react-thunk를 사용해서 이 부분을 정리한다.
Redux-toolkit
Redux를 사용하기 쉽게 만든 개발도구
1. configureStore()
redux의 createStore를 추상화한 store 생성하는 함수
reducer, middleware, devTools, preloadedState, enchancer 정보 전달
기본 미들웨어로 redux-thunk를 추가하고 개발 환경에서 리덕스 개발자 도구(Redux DevTools Extension)를 활성화
const store = configureStore({
reducer: {
counter: counterReducer,
auth: authReducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== 'production',
preloadedState,
enhancers: [reduxBatch],
});
reducer
- 단일 함수를 전달해 루트 리듀서 사용, 슬라이스 리듀서들로 구성된 객체 전달해 생성 가능
- reducer 내부에선 combineReducers 함수로
middleware
- 리덕스 미들웨어를 담는 배열
- 사용자 지정 미들웨어와 기본값을 함께 사용하려면 콜백 표기법을 사용,
- 사용 예) redux-logger, custom middleware, verified serialization
devTools
- 리덕스 개발자 도구 on/off
preloadedState
- store 초기값
enchaners
- 기본 : 배열 , 콜백 함수로 작성 시 미들웨어가 적용되는 순서보다 앞서서 추가 가능
2. createReducer()
상태 변화를 일으키는 리듀서 함수 생성하는 함수
- 내부적으로 immer 라이브러리를 사용하여 불변 업데이트가 이루어지도록 한다
- 액션 처리 시 1) builder callback, 2) map object
- 타입스크립트와 호환성을 위해성 builder callback 표기법 선호
자세한 사항은 redux-toolkit document 참조
3. createAction()
리덕스는 일반적으로 액션 타입 상수, 액션 생성자 함수를 분리하여 선언
-> createAction 함수를 사용해 하나로 결합해 추상화
4. createslice()
리듀서, 액션 생성자, 액션 타입 자동으로 생성
const cartState = {
...
};
const cartSlice = createSlice({
name: 'cart',
initialState: cartState,
reducers: {
addItemToCart(state, action) {
...
},
removeItemFromCart(state, action) {
...
},
},
});
// const { actions, reducer } = cartSlice
// export const { addItemToCart } = actions
// export default reducer
export const cartActions = cartSlice.actions;
export default cartSlice;
redux를 구현하기 위해선 actionsTypes, actions, reducer 가 필요해 구조 중심으로 나눠서 관리하는데 기능을 수정하기 위해선 전부 다른 폴더로 관리되는 파일을 수정해야 한다
createSlice는 Ducks Pattern을 사용해 구조 중심 -> 기능 중심형태로 코드 작성을 돕는다.
ducks-modular-redux - github으로 가면 관련된 내용 확인할 수 있다.
Ducks Pattern
기능 중심으로 관리 -> module이라 부른다.
해당 패턴은 4가지 규칙을 가지고 있다.
- MUST export default a function called reducer()
- MUST export its action creators as functions
- MUST have action types in the form npm-module-or-app/reducer/- ACTION_TYPE
- MAY export its action types as UPPER_SNAKE_CASE, if an external reducer needs to listen for them, or if it is a published reusable library
잘 정리된 블로그가 있어서 링크로 남긴다 React Redux, Ducks Pattern 적용하기 - tlatjdgh3778
Side Effects and Async Tasks in Redux
비동기 처리를 위해 2가지 방법 사용 가능하다
1. Inside the components - useEffect()
Component 내에서 직접 fetch, axios 사용
2. Inside the actions creators
리덕스 툴킷을 통해 action creator 생성 -> 디스 패치 할 작업 크리에이터 생성(Thunk)
Thunk란?
다른 작업이 완료될 떄 까지 작업을 지연시키는 함수
thunk로 action creator를 생성한다.
(Redux Toolkit을 사용 할 경우 기본적으로 thunk 추가 한다.)
https://github.com/reduxjs/redux-thunk
React Route
라우팅이란?
사용자가 요청한 URL에 따라 알맞는 페이지 노출
React-Router
가장 최신버전인 v6로 진행
- 패키지 관리 툴로 react-router-dom 설치
npm install react-router-dom@5
- 프로젝트에 적용
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
App 컴포넌트를 BrowserRouter로 감싸준다. (App 컴포넌트 내부에서도 적용 가능)
- Routers, Route 컴포넌트로 경로와 컴포넌트 설정
Route 컴포넌트 사용 방법
<Route path="주소" element={컴포넌트 JSX 요소} />
// App.js
import { Route, Routes } from "react-router-dom";
import Welcome from "./pages/Welcome";
import Products from "./pages/Products";
const App = () => {
return (
<Routes>
<Route path="/welcome" element={<Welcome />} />
<Route path="/products" element={<Products />} />
</Routes>
);
};
export default App;
- Route 컴포넌트를 사용하기 위해선 Routes 컴포넌트로 감싸줘야 한다.
- 경로 뒤에 “/welcome/*” 처럼 *추가 시 남은 전체 패턴에 대해 라우팅 가능
(맨 끝에서만 사용 가능 - react router 공식 문서)
Link 컴포넌트
페이지를 새로 불러오는 것을 막고 History API를 통해 브라우저 주소의 경로만 바꾸는 기능을 가진 컴포넌트
(a 태그 사용)
사용 방법
<Link to="경로">{text}</Link>
NavLink 컴포넌트
링크에서 사용하는 경로가 현재 라우트 경로와 일치하는 경우 특정 css를 적용하는 컴포넌트
사용 방법
<NavLink
className={(navData) => console.log(navData)}
// className={(navData) => (navData.isActive ? classes.active : "")}
to="welcome"
>
Welcome
</NavLink>
className은 isActive, isPending 2개의 값이 넘어온다
- isActive가 true일 때 -> 유저가 NavLink의 URL 안에 있을 때
- isPending가 true일 때 -> active가 막 되려고 할 때, 즉 데이터가 로딩되고 있을 때
URL 파라미터와 쿼리 스트링
URL 파라미터 : 주소 경로에 값을 넣는 형태
-> useParams() Hook을 이용해 객체 형태로 값 조회
쿼리 스트링 : ? 문자열 이후에 key=value 형태의 값
-> useLocation(), useSearchParams() Hook을 이용해 값 조회
// Parent Component
<Route path="/products/:productId" element={<ProductDetail />} />
// child Component
const ProductDetail = () => {
const params = useParams();
const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
console.log(params);
console.log(location);
console.log(searchParams.get('name'));
console.log(searchParams.get('name2'));
return (
<section>
<h1>Product Detail</h1>
<p>{params.productId}</p>
</section>
);
};
export default ProductDetail;
내부 중첩 route
-
부모 컴포넌트에서 * wildcart를 사용해 해당 패턴에 대한 라우팅 설정
-
자식 컴포넌트에서 Route 태그 추가
// parent
<Route path="/welcome/*" element={<Welcome />} />
// child
const Welcome = () => {
return (
<section>
<h1>The Welcome Page</h1>
<Routes>
<Route path="new-user" element={<p>Welcome, new user!</p>} />
</Routes>
</section>
);
};
Redirect
Navigate Component 사용
사용 방법
<Route path="/" element={<Navigate to="/welcome" />} />
useNavigate
다른 페이지로 이동할 때 사용하는 Hook
const Layout = () => {
const navigate = useNavigate();
const goBack = () => {
// 이전 페이지로 이동
navigate(-1);
};
const goMain = () => {
// main 경로로 이동
navigate('/main');
};
const goMainReplace = () => {
navigate('/main', {replace: true});
}
return (
<div>
...
</div>
);
};
export default Layout;
다른 페이지로 이동 시 replace 옵션을 true 설정 할 경우 리다이렉션이 된다.
(리다이렉트가 되면 브라우저 내 페이지 기록에 남기지 않는다.)
Navigate Guard
v5에선 Prompt 컴포넌트를 사용해 페이지 이동을 막고, 제어할 수 있다.
현재 v6에선 기능이 동작되지 않고 구현 중이므로 구현해서 사용해야 한다
[V6] [Feature] Getting usePrompt and useBlocker back in the router #8139
Block user navigation with React Router v6 - Tadas Goberis
이에 관련된 내용
React LazyLoading
React앱을 화면에 렌더링 하기 위해선 필요한 코드를 다운받아 화면에 노출시키는데 앱의 크기가 커지면 다운로드 할 파일이 커져 페이지 노출하는데 시간이 걸린다.
React는 코드를 분할해 화면상 보이는 페이지만 로드할 수 있게 **지연로딩(lazy-loading)**을 지원한다.
-
컴포넌트를 React.lazy()로 동적 import 한다.
-
Suspense컴포넌트로 lazy컴포넌트를 감싸준다.
const NotFound = React.lazy(() => import('./pages/NotFound'));
function App() {
return(
<Layout>
<Suspense
fallback={
<div className="centered">
<LoadingSpinner />
</div>
}
>
...
<Route path="*">
<NotFound />
</Route>
</Suspense>
</Layout>
)
}
React Animation
리액트 컴포넌트에 애니메이션 적용 시 오픈 할 땐 문제가 되지 않지만 제거 시, 컴포넌트가 바로 삭제되기 때문에 추가 라이브러리 필요
-> react-transition-group (368, 370, 373듣고 간단히 정리)
react-transition-group은 dom에서 요소를 제거하거나 추가할 때 있어 시간과 관련 된 것들을 제공
react-motion, react-move, react-router-transition -> 애니메이션 적용 시 고려
Testing
Jest
자바스크립트 테스트 프레임워크 -> 유닛 테스트, 스냅샷 비교, mocking, coverage 기능 제공
Global Variable
Jest는 테스트 파일에서 사용하는 메서드와 객체를 전역환경에 배치 (expect, jest, test …)
명시적으로 가져 올 경우
import { expect, jest, test } from '@jest/globals'
1. test
test 할 때 사용하는 메소드, it 메소드 동일한 기능
// test(name, fn, timeout)
test('name', () => {})
// it(name, fn, timeout)
it('name', () => {})
2. describe
여러개의 테스트 그룹화
describe('group', () => {
test('test1', () => {
...
});
test('test2', () => {
...
});
});
3. expect
테스트 시 항목의 유효성 검사할 수 있는 matcher 함수 제공
4. jest 객체
몇가지 유틸리티 함수를 가지는데 대표적으로 fn함수 (Mock데이터 생성)
React testing library
React UI Component test 하는데 사용 (가상 DOM 제공)
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@testing-library/jest-dom'
import Fetch from './fetch'
test('loads and displays greeting', async () => {
// ARRANGE
render(<Fetch url="/greeting" />)
// ACT
await userEvent.click(screen.getByText('Load Greeting'))
await screen.findByRole('heading')
// ASSERT
expect(screen.getByRole('heading')).toHaveTextContent('hello there')
expect(screen.getByRole('button')).toBeDisabled()
})
1. render 객체
테스트를 위해 특정 컴포넌트를 jsdom에 렌더링
2. Query - screen 객체
렌더링 된 DOM 노드에 접근해 엘리먼트를 가져오는 메서드
메서드 네이밍
{쿼리 타입}{다수의 타겟 탐색 시 - all 추가}{타겟 유형}
// ex)
getAllByRole
- 쿼리 타입 : get(동기적), find(비동기적 / 프로미스 반환)
- 타겟 유형 : ex) ByRole, ByText …
3. Action - userEvent객체
얻어온 타겟을 이용해 이벤트 실행
ex) click
Tips!!
1. UI 테스트 시 서버 요청 x -> 요청에 따른 결과값으로 컴포넌트 동작 테스트 코드 작성 o
요청하는 코드를 재사용x (서버 부하) -> Mock 데이터를 생성해서 테스트 한다.
2. React Custom Hook
custom hook 테스트를 간단하게 해준다
react-hooks-testing-library - Github
react-hooks-testing-library - 공식문서
Reference
- Udemy - React 완벽 가이드 with Redux, Next.js, TypeScript
- 3가지 원칙 - redux doc
- Style Guide: Best Practices - redux doc
- 용어집 - redux doc
- usage-guide - Redux-toolkit doc
- Redux Toolkit (리덕스 툴킷)은 정말 천덕꾸러기일까? - 화해 블로그
- React Redux, Ducks Pattern 적용하기 - tlatjdgh3778
- 벨로퍼트와 함께하는 모던 리액트
- React Router 공식 문서
- 코드 분할 - react 공식 문서
- jest 공식 문서
- React testing library 공식 문서