Ian's Archive 🏃🏻

thumbnail
React Redux, Routing
React
2022.09.05.

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

장점

  • 상태를 예측 가능하게 만듬
  • 테스트나 디버깅에 용이

특징

  1. Single source of truth

    애플리케이션의 모든 상태는 하나의 저장소 안에 하나의 객체 트리 구조로 저장

  2. State is read-only

    상태는 읽기전용(불변) 데이터이며, 오직 액션만 상태를 변경할 수 있다

  3. 변화를 일으키는 함수, 리듀서는 순수 함수여야 한다

    이전 상태와 액션을 받아 다음 상태를 반환하는 순수 함수

순수함수: 반환(reture)값이 전달 인자(argument) 값에만 의존하는 함수

=> 순수 함수, 사이드 이펙트 x, 비동기 코드x

핵심 개념

Flux패턴

단방향 데이터 흐름의 디자인 패턴

copyButtonText
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 사용 방법

  1. Action 선언 및 Reducer 함수 작성
copyButtonText
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);
  1. Provider 컴포넌트를 사용해 store 연동
copyButtonText
root.render(
    <Provider store={store}>
        <App />
    </Provider>,
);
  1. Store내부의 State사용을 위한 useSelector액션을 실행 시킬 useDispatch 사용
  2. Component에 이벤트 - 함수 트리거
copyButtonText
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로 가져온 액션 호출

copyButtonText
// 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에 데이터를 등록한 상태를 가져오기 위한 함수

copyButtonText
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)를 활성화

copyButtonText
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

Redux Toolkit (리덕스 툴킷)은 정말 천덕꾸러기일까? - 화해 블로그

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()

리듀서, 액션 생성자, 액션 타입 자동으로 생성

copyButtonText
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

copyButtonText

React Route

라우팅이란?

사용자가 요청한 URL에 따라 알맞는 페이지 노출

React-Router

가장 최신버전인 v6로 진행

  1. 패키지 관리 툴로 react-router-dom 설치
copyButtonText
npm install react-router-dom@5
  1. 프로젝트에 적용
copyButtonText
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 컴포넌트 내부에서도 적용 가능)

  1. Routers, Route 컴포넌트로 경로와 컴포넌트 설정

Route 컴포넌트 사용 방법

copyButtonText
<Route path="주소" element={컴포넌트 JSX 요소} />
copyButtonText
// 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 공식 문서)

페이지를 새로 불러오는 것을 막고 History API를 통해 브라우저 주소의 경로만 바꾸는 기능을 가진 컴포넌트
(a 태그 사용)

사용 방법

copyButtonText
<Link to="경로">{text}</Link>

링크에서 사용하는 경로가 현재 라우트 경로와 일치하는 경우 특정 css를 적용하는 컴포넌트

사용 방법

copyButtonText
<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을 이용해 값 조회

copyButtonText
// 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;

useSearchParams 공식 문서

내부 중첩 route

  1. 부모 컴포넌트에서 * wildcart를 사용해 해당 패턴에 대한 라우팅 설정

  2. 자식 컴포넌트에서 Route 태그 추가

copyButtonText
// 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 사용

사용 방법

copyButtonText
<Route path="/" element={<Navigate to="/welcome" />} />

useNavigate

다른 페이지로 이동할 때 사용하는 Hook

copyButtonText
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 설정 할 경우 리다이렉션이 된다.
(리다이렉트가 되면 브라우저 내 페이지 기록에 남기지 않는다.)

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)**을 지원한다.

  1. 컴포넌트를 React.lazy()로 동적 import 한다.

  2. Suspense컴포넌트로 lazy컴포넌트를 감싸준다.

copyButtonText
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 …)

명시적으로 가져 올 경우

copyButtonText
import { expect, jest, test } from '@jest/globals'

더 많은 글로벌 변수 확인(jest 공식 문서)

1. test

test 할 때 사용하는 메소드, it 메소드 동일한 기능

copyButtonText
// test(name, fn, timeout)
test('name', () => {})

// it(name, fn, timeout)
it('name', () => {})

2. describe

여러개의 테스트 그룹화

copyButtonText
describe('group', () => {
  test('test1', () => {
    ...
  });

  test('test2', () => {
    ...
  });
});

3. expect

테스트 시 항목의 유효성 검사할 수 있는 matcher 함수 제공

4. jest 객체

몇가지 유틸리티 함수를 가지는데 대표적으로 fn함수 (Mock데이터 생성)

matchers 함수 예시

React testing library

React UI Component test 하는데 사용 (가상 DOM 제공)

copyButtonText
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 노드에 접근해 엘리먼트를 가져오는 메서드

메서드 네이밍

copyButtonText
{쿼리 타입}{다수의 타겟 탐색  - 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

Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.