선언적 vs 명령적 차이

 

  • useQuery (선언적)를 쓴다면 react-query에서 제공하는 여러 기능들 (ex. refetchOnWindowFocus, enabled)이 "알아서" 돌아가지만,
  • useMutation (명령적)의 경우는 이런 기능들이 "알아서" 돌아가지 않습니다.

 

이유는 useQuery의 경우 선언하는 시점에 observer가 생성되지만 useMutation은 mutate을 사용하기 전까지 observer가 생성되지 않습니다. 또한 useMutation의 observer는 mutate가 돌아갈때마다 이전 observer를 unsubscribe 합니다.

 

1. useQuery만 사용
useQuery(['someList', page, keyword],
() => {
const params = { page, keyword, };
fetchList(params);
},
{
onSuccess: () => { // do something }
});

2. useQuery, useMutation, useEffect 사용
useQuery('someList',
() => {
const params = { page, keyword, };
fetchList(params);
},
{
onSuccess: () => { // do something }
});

const { mutate } = useMutation(fetchList,
{
onSuccess: () => { queryClient.invalidateQueries('someList'); }
});

useEffect(() => {
const params = { page, keyword, };
mutate(params);
}, [page, keyword]);

 

1번 방법이 좀 더 일반적인 방법인 것 같습니다.

아시다시피 useMutation은 CUD을 위한 훅이고 useMutation으로 GET 요청을 하는 것은 설계 용도를 벗어나는 사용법이 아닌가 싶습니다. 우선 1번과 2번의 차이는 1번의 경우 param이 바뀔때 서버로 요청을 1번만 보내게 됩니다. 2번의 경우는 2번 보냅니다. mutate를 할때 fetchList로 한번 보내고 이후 "someList" 키가 invalidate 되면서 useQuery가 한번 더 fetchList를 호출합니다. 이론적으로 useQuery는 여러 컴포넌트에서 호출해도 같은 값을 공유하지만, useMutation의 경우는 값을 공유하지 않는 것으로 알고 있습니다. 다만, mutation의 상태를 저장/관리하기 위해 MutationCache라는 곳에 mutation 자체와 관련 데이터를 따로 저장하는데요. 이걸 감안한다면 2번 방법은 메모리도 더 많이 쓰는 방법이 되는 것 같네요.

 

정리를 하자면:

- 1번 방법이 일반적인 방법인 것 같습니다. query key에 page, keywords를 넣어서 관리해도 무방해요.

- 2번 방법이 메모리 차원에서 더 비효율적입니다

- 2번 방법은 네트워크 요청을 2번 보냅니다

- 2번 방법은 훅이 설계된 의도대로 사용되지 않아서, 버그를 발생 시킬 수 있습니다

- (사견) 2번 방법은 코드 의도를 파악하기 쉽지 않은 것 같습니다

- (사견) page, keyword 조합마다 쿼리가 저장될텐데, 캐싱이 꼭 필요한건지/유용하게 사용할 수 있는지도 고민이 필요한거 같습니다

 

1번 방식은 useQuery hook을 사용하여 새로운 page 또는 keyword가 전달되면 자동으로 API를 호출하여 데이터를 업데이트 합니다. 즉, React Query의 기능만으로도 데이터를 재조회할 수 있습니다. onSuccess 옵션을 사용하여 API 호출에 대한 처리를 추가할 수 있습니다.

 

2번 방식은 useMutation과 useEffect를 사용하여 데이터를 재조회합니다. useMutation을 사용하여 API를 호출하고, onSuccess 옵션을 사용하여 API 호출에 대한 처리를 추가합니다. useEffect를 사용하여 페이지나 검색어가 변경될 때마다 useMutation을 호출하고, invalidateQueries를 사용하여 데이터를 다시 조회합니다.

'Framework > React' 카테고리의 다른 글

[React] useQuery와 useQueries  (0) 2024.10.14
[React] useQuery  (1) 2024.10.10
[React] React-query  (0) 2024.10.07
[React] 리덕스(Redux)  (1) 2024.10.07
[React] React Router - BrowserRouter  (0) 2024.10.04

리액트 쿼리를 왜 사용하는가?

React Query는 애플리케이션의 데이터를 관리하고 동기화하는데 사용 되는 라이브러리로 최근 많은 개발자들에게 인기를 얻고 있는 상태관리 라이브러리 입니다. 리액트 쿼리를 사용하는 이유는 크게 다음과 같습니다.

 

1) 간편한 데이터 관리

데이터 가져오기, 캐싱, 동기화 및 업데이트 처리를 간편하게 할 수있게 해줍니다.

2) 실시간 업데이트 및 동기화

실시간 데이터 업데이트와 자동 동기화를 지원하여 서버와 클라이언트 데이터의 일관성을 유지합니다.

3) 데이터 캐싱

데이터를 캐싱하여 불필요한 API 요청을 줄이고 애플리케이션의 성능을 향상 시킵니다.

4) 서버 상태 관리

서버 상태 관리 (예를들면 로딩중, 에러, 성공 등의 상태)를 간편하게 처리할 수 있습니다.

5) 간편한 설정

React Query는 간단한 설정으로 사용할 수 있습니다.

리액트 쿼리를 사용하기 전에 먼저 @tanstack/react-query와 devtools 를 설치해줍니다.

(devtools 는 선택사항이며 devtools을 사용하면 패칭한 데이터를 쉽게 관리할 수 있습니다.)

 

최상위 컴포넌트 위에 QueryClientProvider로 감싸줘야 합니다.

(devtools는 최상위 컴포넌트에 최대한 가까운 위치로 배치해주시면 됩니다.)

 

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement,
);

const store = configureStore();
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false, // refetch 조건 막기
      retry: 0, // 요청 실패했을 경우 재요청 횟수
    },
  },
});

root.render(
  <Provider store={store}>
    <QueryClientProvider client={queryClient}>
      <ReactQueryDevtools initialIsOpen={true} />
            <App />
    </QueryClientProvider>
  </Provider>,
);

 

devtools를 설치하게 되면 화면 하단에 꽃모양의 아이콘이 생기게 됩니다.

클릭해보면 현재 불러오고 있는 데이터들이 무엇이 있는지 한눈에 보며 관리할 수 있습니다.

 

 

정리

 

react-query의 장점에는 데이터를 캐시해서 전달하는 것이 있다.
쉽게 말해서 react-query가 자체적으로 서버 데이터를 저장하고 있다가 api 요청 없이 해당 데이터를 보여준다는 것이다.

 

이 부분에서 react-query stale fresh라는 개념을 사용하는데, 간단하게 음식을 생각하면 좋다.

 

1.유저 react-query한테 국밥(data)를 요청
2-1. react-query는 캐시에 신선한(fresh)국밥이 있으면 그대로 줌
2-2. 캐시에 상한(stale)국밥이나, 국밥 자체가 없으면 서버에 국밥(data) 받아와서 줌

 

 

isLoading이랑 isFetching의 차이

 

  • isLoading은 서버에 데이터 요청을 처음 할 때
  • isFetching은 서버에 데이터 요청을 다시 할 때 (캐시된 데이터가 있을 때)

 

isFetching

isFetching은 어떠한 react-query요청 내부의 비동기 함수가 처리되었는지 여부 에 따라 true/false로 나누어 진다.

 

isLoading

isLoading은 캐시된 데이터조차 없이, 처음 실행된 쿼리 일 때 로딩 여부에 따라 true/false로 나누어 진다.

즉, 결론적으로 isLoading과 isFetching은 비슷하게 '로딩' 이라는 개념을 사용하지만 기존에 캐시된 데이터가 있느냐 에 따라 다르다.

 

간단하게 생각하자면 isLoading은 어떤 데이터를 처음 가져올 때 사용하면 되고,
isFetching은 데이터를 다시 가져와야 할 때 사용하면 된다.

 

국밥을 처음 가져올때는 숟가락, 젓가락, 물도 가져다줘야 하지만, (isLoading)
다시 한번 주문하면 국밥만 가져다 준다. (isFetching)

 

이처럼 두 경우에 있어서 작업의 차이가 필요할 때 사용 한다고 보면 되겠다.

 

정리하면

isLoading은 React Query에서 쿼리 수준에서 사용되는 속성이고 특정 쿼리가 현재 데이터를 가져오고 있는지 여부를 나타내고 특정 쿼리에 대한 로드 상태를 조건부로 처리할 수 있다.
isFetching은 React Query에서 전역 수준에서 사용할 수 있는 메서드이다. 애플리케이션의 쿼리가 현재 데이터를 가져오고 있는지 여부를 나타내고. 여러 쿼리에서 가져오는 데이터의 전체 로드 상태를 결정하는 방법을 제공한다.

'Framework > React' 카테고리의 다른 글

[React] useQuery  (1) 2024.10.10
[React] useQuery와 useMutation  (0) 2024.10.10
[React] 리덕스(Redux)  (1) 2024.10.07
[React] React Router - BrowserRouter  (0) 2024.10.04
[React] React Helmet  (0) 2024.10.04

참고 자료

https://ko.redux.js.org/introduction/getting-started

 

Redux 시작하기 | Redux

소개 > 시작하기: Redux를 배우고 사용하기 위한 자료

ko.redux.js.org

 

리덕스(Redux)란?

  • 여러 컴포넌트가 공유하는 상태를 관리하기 위한 라이브러리로, 메타(페이스북)가 설계한 flux 규격에 맞추어져 있다.
  • 리액트와 사용하기 위해서는 RTK(React Tool Kit)을 함께 설치해야 한다.
  • 리액트 컨텍스트에 기반을 둔 라이브러리로 Provider 컴포넌트가 항상 취상위로 동작해야 한다.
    (react-redux는 Provider컴포넌트를 제공한다)
- "전역" 상태를 포함하는 단일 스토어
- 앱에 어떤 일이 일어날 때 스토어에 일반 객체 액션을 디스패치하는 것
- 액션을 살펴보고 불변성을 유지한 채 업데이트된 상태를 반환하는 순수 리듀서 함수

 

Redux 가 일반적으로 가지고 있는 구성 요소

  • 액션 객체를 생성하는 액션 생성자
  • 부수 효과를 가능하게 하는 미들웨어
  • 부수 효과를 가진 동기 또는 비동기 로직을 포함하는 Thunk 함수
  • ID로 항목 조회를 가능하게 하는 정규화된 상태
  • Reselect 라이브러리를 사용하여 파생된 데이터를 최적화하는 메모이제이션된 셀렉터 함수
  • 액션의 이력과 상태 변경을 확인할 수 있는 Redux DevTools 확장 프로그램
  • 액션, 상태 및 기타 함수에 대한 TypeScript 타입

추가적으로, Redux는 보통 React-Redux 라이브러리와 함께 사용되어 React 컴포넌트가 Redux 스토어와 상호 작용할 수 있게 합니다.

 

Redux를 사용하는 이유

먼저 상태란?

  • React에서 State는 component 안에서 관리되는 것이다.
  • 자식 컴포넌트들 간의 다이렉트 데이터 전달은 불가능 하다.
  • 자식 컴포넌트들 간의 데이터를 주고 받을 때는 상태를 관리하는 부모 컴포넌트를 통해서 주고 받는다.
  • 그런데 자식이 많아진다면 상태 관리가 매우 복잡해진다.
  • 상태를 관리하는 상위 컴포넌트에서 계속 내려 받아야한다. => Props drilling 이슈

리덕스를 쓰면, 상태 관리를 컴포넌트 바깥에서 한다!

리덕스를 사용하면 상태값을, 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트의 바깥에서 관리 할 수 있게 된다.

1. 전역 상태 저장소 제공

2. Props drilling 이슈 해결

 

Redux의 장점

  • 순수 함수를 사용해 상태를 예측 가능하게 만듭니다.
  • 유지보수에 용이합니다. 
  • redux dev tool이 있어 디버깅에 유리합니다.
  • 비동기를 지원하는 Redux Sage, Redux Thunk 등 다양한 미들웨어가 존재합니다.

 

Redux는 언제 쓰는 게 좋을까?

  • 전역 상태가 필요하다고 느껴질 때
  • 상태들이 자주 업데이트 될 때 
  • 상태를 업데이트 하는 로직이 복잡할 때
  • 앱이 크고 많은 사람들에 의해 코드가 관리될 때
  • 상태가 업데이트되는 시점을 관찰할 필요가 있을 때

 

Redux 흐름

1. 전역 상태 저장소 제공

2. Props drilling 이슈 해결

 

🧺 Store (스토어)

Store(스토어)는 상태가 관리되는 오직 하나의 공간이다.

  • 컴포넌트와는 별개로 스토어라는 공간이 있어서 그 스토어 안에 앱에서 필요한 상태를 담는다.
  • 컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한다.

📃 Action (액션)

  • Action(액션)은 앱에서 스토어에 운반할 데이터를 말한다. (주문서)
  • Action(액션)은 자바스크립트 객체 형식으로 되어있다.

🎉 Reducer (리듀서)

  • Action(액션)을 Store(스토어)에 바로 전달하는 것이 아니다.
  • Action(액션)을 Reducer(리듀서)에 전달해야한다.
  • Reducer(리듀서)가 주문을 보고 Store(스토어)의 상태를 업데이트하는 것이다.
  • Action(액션)을 Reducer(리듀서)에 전달하기 위해서는 dispatch() 메소드를 사용해야한다.

🎉 Dispatch (디스패치)

  • Dispatch는 store의 내장 함수 중 하나로, action을 발생시킵니다.
  • action을 파라미터로 전달하고 reducer를 호출합니다.

🎉 Subscribe (구독)

  • Subscribe는 store의 내장 함수 중 하나로, 특정 함수를 전달해주면 action이 dispatch 되었을 때마다 전달된 함수가 호출됩니다.

 

  • Action(액션) 객체가 dispatch() 메소드에 전달된다.
  • dispatch(액션)를 통해 Reducer를 호출한다.
  • Reducer는 새로운 Store 를 생성한다.

예를 들어서 B 에서 일어나는 변화가 G 에 반영된다고 가정을 해보자.

 

1.스토어 설정

https://velopert.com/3528

리덕스를 프로젝트에 적용하게 되면 이렇게 스토어가 생긴다. 스토어 안에는 프로젝트의 상태에 관한 데이터들이 담겨있다.

2. 컴포넌트의 스토어 구독

https://velopert.com/3528

G 컴포넌트는 스토어에 구독을 한다. 구독을 하는 과정에서, 특정 함수가 스토어한테 전달이 된다. 그리고 나중에 스토어의 상태값에 변동이 생긴다면 전달 받았던 함수를 호출해준다.

3. 스토어에 상태 변경하라고 알려주기

이제 B 컴포넌트에서 어떤 이벤트가 생겨서, 상태를 변화 할 일이 생긴다. 이 때 dispatch 라는 함수를 통하여 액션을 스토어한테 던져준다. 액션은 상태에 변화를 일으킬 때 참조 할 수 있는 객체다. 액션 객체는 필수적으로 type 라는 값을 가지고 있어야 한다.

4. 리듀서를 통하여 상태를 변화시키기

https://velopert.com/3528

액션 객체를 받으면 전달받은 액션의 타입에 따라 어떻게 상태를 업데이트 해야 할지 정의를 해줘야 한다. 이러한 업데이트 로직을 정의하는 함수를 리듀서라고 부른다.

 

리듀서 함수는 두가지의 파라미터를 받습니다.

  1. state: 현재 상태
  2. action: 액션 객체

그리고, 이 두가지 파라미터를 참조하여, 새로운 상태 객체를 만들어서 이를 반환합니다.

 

5. 상태가 변화가 생기면, 구독하고 있던 컴포넌트에게 알림

https://velopert.com/3528

상태에 변화가 생기면, 이전에 컴포넌트가 스토어한테 구독 할 때 전달해줬었던 함수 listener 가 호출된다. 이를 통하여 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링을 한다.

 

redux의 원칙

Single Source of truth

  • 동일한 데이터는 항상 같은 곳에서 가져옵니다.
  • 즉, 스토어라는 하나뿐인 데이터 공간이 있다는 의미입니다.

 

State is read-only

  • 리액트에서는 setState 메소드를 활용해야만 상태 변경이 가능합니다.
  • 리덕스에서도 액션이라는 객체를 통해서만 상태를 변경할 수 있습니다.

 

Changes are made with pure functions

  • 변경은 순수함수로만 가능합니다.
  • 이는 리듀서와 연관되는 개념입니다.
  • Store - Action - Reducer

 

store 만들기

1. Provider 컴포넌트를 적용한다. Provider는 필수적으로 store를 지정해줘야 한다.

store란 이름 그대로 상태 정보를 객체로 정리해서 관리한다.

ConfigureStoreOptions 타입(인터페이스)이며, 파라미터로 reducer, middleware, devTools, reloadedState, enhancers 를 갖는다. 이중 리듀서 외에는 모두 옵셔널하다. 아래에서 더 디테일하게 다룰거지만 우선 redux toolkit에서 제공하는 configureStore 메서드를 사용하면 된다. const store = configureStore({ reducer: myReducerus });
이 다음에 리덕스의 구성요소를 하나하나 뜯어보며 새로 선언해서 구성해보자

// ./src/index.tsx

import { Provider } from 'react-redux';

root.render(
	{/* myReducer는 아직 없다. 새로 선언해야 한다 */}
	const store = configureStore({ reducer: myReducer });
  	{/* 이 store는 임시, 따로 분리해서 새로 선언할 것이다 */}
  return (
    <Provider store={store}>
      <main>
				{/* 여기에 컴포넌트 추가^^ */}
      </main>
    </Provider>
);

 

2. store를 분리해서 따로 구현하기 전에 먼저 Reducer, type state, Action 을 만든다.

1. Redux Store가 저장할 상태 정보(State), Redux Store는 내 상태 정보를 저장한다.
2. 그리고 새 상태를 반환하는 함수인 리듀서(Reducer)를 사용한다. 즉, 전역적으로 상태를 관리할 데이터 컨테이너인 store를 만들 것이며, 여기서 관리할 상태 데이터 타입을 선언하고, 상태 데이터를 새로 바꾸기 위한 리듀서를 만들어야 한다. 리듀서는 현재 state와 Action을 변수로 받아서 새로운 state를 반환한다.

 

src/store/configureStore.ts

import { legacy_createStore as createStore } from 'redux';

import rootReducer from './reducers';

const configureStore = (): any => createStore(rootReducer);

export default configureStore;

 

RootReducer 정의

  • 여러 reducer을 사용하는 경우 reducer을 하나로 묶어주는 메소드
src/store/reducer.ts

import { combineReducers } from 'redux';

import main from '../redux/reducers/mainReducer';
import app from '../redux/reducers/appReducer';
import login from '../redux/reducers/loginReducer';
import tenant from '../redux/reducers/tenantReducer';
import mybenefit from '../redux/reducers/myBenefitReducer';
import receipt from '../redux/reducers/receiptReducer';
import snowpoint from '../redux/reducers/snowPointReducer';
import user from '../redux/reducers/userReducer';

const rootReducer = combineReducers({
  main,
  app,
  tenant,
  login,
  mybenefit,
  receipt,
  snowpoint,
  user,
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

 

세부 reducer 정의

src/redux/reducers/mainReducer.ts

import { E_POPUP_STATE } from 'src/utils/enum';
import ActionTypes from '../constants/mainConstants';

import { E_MAIN_MENU_STATE, MainAction, TMainState } from '../types/mainTypes';

export const initialState: TMainState = {
  mainMenuState: E_MAIN_MENU_STATE.NONE,
  isOpenMartSearchPopup: E_POPUP_STATE.NONE,
};

function mainReducer(
  state: TMainState = initialState,
  action: MainAction,
): TMainState {
  switch (action.type) {
    case ActionTypes.OPEN_MAIN_MENU:
      return {
        ...state,
        mainMenuState: E_MAIN_MENU_STATE.OPEN,
      };
    case ActionTypes.CLOSE_MAIN_MENU:
      return {
        ...state,
        mainMenuState: E_MAIN_MENU_STATE.CLOSE,
      };
    case ActionTypes.NONE_MAIN_MENU:
      return {
        ...state,
        mainMenuState: E_MAIN_MENU_STATE.NONE,
      };
    case ActionTypes.OPEN_MART_SEARCH_POPUP:
      return {
        ...state,
        isOpenMartSearchPopup: E_POPUP_STATE.OPEN,
      };
    case ActionTypes.CLOSE_MART_SEARCH_POPUP:
      return {
        ...state,
        isOpenMartSearchPopup: E_POPUP_STATE.CLOSE,
      };
    default:
      return state;
  }
}

export default mainReducer;

 

컴포넌트에서 redux 사용하기

import { useSelector, useDispatch } from "react-redux";
import { increseCount } from "reducers/count";

// dispatch를 사용하기 위한 준비
const dispatch = useDispatch();

// store에 접근하여 state 가져오기
const { count } = useSelector(state => state.counter);

const increse = () => {
  // store에 있는 state 바꾸는 함수 실행
  dispatch(increseCount());
};

const Counter = () => {
  return (
    <div>
      {count}
      <button onClick={increse}>증가</button>
    </div>
  );
};

export default Counter;

 


 

실제 적용 예시

 

actions 폴더

appAction.ts

loginAction.ts... 등등은 정리해놓은 action을 모아놓은 파일 

 

appAction.ts

/**
 * @name 알림 팝업 열기
 */
export const actionOpenAlertPopup = (
  alertPopupState: TAlertPopupProps,
): AppAction => {
  return action(ActionTypes.OPEN_ALERT_POPUP, { alertPopupState });
};

/**
 * @name 알림 팝업 닫기
 */
export const actionCloseAlertPopup = (): AppAction => {
  return action(ActionTypes.CLOSE_ALERT_POPUP);
};

 

인자(타입 지정)를 받을 수도 있고 안 받을 수도 있고 그걸 어떤 action으로 return해줄건지 정리

 

 

constansts 폴더

enum ActionTypes {
  OPEN_ALERT_POPUP = 'app/OPEN_ALERT_POPUP',
  CLOSE_ALERT_POPUP = 'app/CLOSE_ALERT_POPUP',
}

export default ActionTypes;

 

 

액션 타입을 string > 변수로 정의해놓은 폴더

 

reducers 폴더

function AppReducer(
  state: TAppState = initialState,
  action: AppAction,
): TAppState {
  switch (action.type) {
      case ActionTypes.OPEN_ALERT_POPUP:
      return {
        ...state,
        alertPopupState: {
          isOpen: true,
          onClose: action.payload.alertPopupState.onClose,
          children: action.payload.alertPopupState.children,
        },
      };
    case ActionTypes.CLOSE_ALERT_POPUP:
      return {
        ...state,
        alertPopupState: {
          isOpen: false,
          onClose: function (): void {
            return;
          },
        },
      };
      default:
  return state;
  }
}

export default AppReducer;

 

정의된 액션 타입의 행동을 정의함 

 

types 폴더

export type TAppState = {
  confirmPopupState: TConfirmPopupProps;
};

 

타입 모아놓은 폴더

'Framework > React' 카테고리의 다른 글

[React] useQuery와 useMutation  (0) 2024.10.10
[React] React-query  (0) 2024.10.07
[React] React Router - BrowserRouter  (0) 2024.10.04
[React] React Helmet  (0) 2024.10.04
[React] Redux의 configureStore와 Provider  (0) 2024.10.02

일단, 리액트 라우터를 알기 전에 리액트가 SPA를 쉽게 만들수 있도록 하는 라이브러리라는 점에 주목해야한다.

SPA의 경우 브라우저상으로 뒤로 가기, 앞으로 가기와 같은 내비게이션 사용이 불가하다.

 

그 이유는 기존방식은 url 변경시 새로고침되며 모든 페이지를 reload하여 로드 시간이 오래 걸렸지만,

React는 SPA 체제로 새로고침 대신 Router를 사용하여 변경된 소스만 바뀌도록 하기 때문에 속도를 개선한다. 

 

하지만  페이지에 따라 주소를 각각 만들기 위한 Routing이 필요하고 리액트에서는 다양한 라이브러리를 통해 이러한 기능을 제공한다.

 

그러한 라이브러리 중 대표적으로 React Router가 있다.

 

React Router는 여러 화면으로 구성된 웹 애플리케이션을 만들 때 클라이언트 사이드의 라우팅을 간단하게 작업할 수 있도록 도와준다.

 

설치방법은 관리하는 패키지 매니저에 따라 아래와 같이 설치할 수 있다.

npm install react-router-dom
yarn add react-router-dom

이러한 React Router에는 대표적으로 BrowserRouter HashRouter가 있다.

 

 

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <BrowserRouter>
      <App />
  </BrowserRouter>
);

 

또한 Routes와 Route를 통해 url에 따라 렌더링을 다르게 할 수 있다.

// App.tsx

import { Routes, Route, Navigate } from 'react-router-dom';

import Home from 'pages/Home';
import MyPage from 'pages/MyPage';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/mypage" element={<MyPage />} />
      <Route path="*" element={<Navigate to="/" replace={true} />} />
    </Routes>
  );
}

export default App;

이 때, path="*"는 존재하지 않는 경로로 들어올 때(ex: /auth등) 리다이렉트할 수 있게끔 작성하였다. (replace = true는 뒤로갈 수 있게끔 설정했다.)


exact

<Router>
  <nav>
  	<Link to="/">Home</Link>
  	<Link to="/blog">Blog</Link>
  </nav>
  <Route path="/">Home Page</Route>
  <Route path="/blog"><BlogForm /></Route>
</Router>

홈페이지에 들어갔을 때 /blog 도 /로 매칭되기 때문에 2개의 컴포넌트가 모두 렌더링 된다는 것

 

<Router>
  <nav>
  	<Link to="/">Home</Link>
  	<Link to="/blog">Blog</Link>
  </nav>
  <Route exact path="/">Home Page</Route>
  <Route path="/blog"><BlogForm /></Route>
</Router>

 

이와 같은 문제점을 해결하기 위해선 path 앞에 exact를 사용하여 정확히 일치하는, 즉 부분적으로 일치하는 것이 아니라 정확하게 일치하는 URL의 컴포넌트를 렌더링시키는 방법을 사용할 수 있다

 

'Framework > React' 카테고리의 다른 글

[React] React-query  (0) 2024.10.07
[React] 리덕스(Redux)  (1) 2024.10.07
[React] React Helmet  (0) 2024.10.04
[React] Redux의 configureStore와 Provider  (0) 2024.10.02
[React] React Query의 핵심 객체, QueryClient  (0) 2024.10.02
import { Helmet } from "react-helmet-async";
import "./styles.css";
import InnerComponent from "./InnerComponent";
import { useState } from "react";

export default function App() {
  const [shouldShowInner, setShouldShowInner] = useState(false);

  return (
    <div className="App">
      <Helmet>
        <title>인사말</title>
        <link rel="" href="" />
      </Helmet>
      <h1>Hello World</h1>
    </div>
  );
}​
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import { HelmetProvider } from "react-helmet-async";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
    <HelmetProvider>
      <App />
    </HelmetProvider>
);

 

react-helmet과 유사한 라이브러리이다.

 

react-helmet

react-helmet은 컴포넌트 내에서 head 태그를 수정할 수 있게 합니다.
head 태그 내의 title이나 meta 태그 등을 수정하여 여러 기능을 구현할 수 있습니다.

  1. 각 컴포넌트마다 브라우저 문서 탭의 타이틀 변경 가능
  2. 소셜 공유 시 meta 태그 수정
  3. 검색 엔진 최적화

사용함으로써 위와 같은 이점을 얻을 수 있습니다.

 

react-helmet을 쓰지 않는 이유

그러나 react-helmet은 thread-safe하지 않은 react-side-effect에 의존한다는 단점을 가지고 있습니다.
따라서, 비동기 데이터 처리에 문제가 생길 수 있습니다.

* 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음

react-helmet-async는 thread-safe하기 때문에 사용합니다.

 

사용 이유

SPA 프로젝트에 사용되는 라이브러리입니다.
body태그 내부의 태그 및 컴포넌트들을 미리 한꺼번에 수령하여 render할 수 있으나,
SSR이 아니기 때문에 head 태그 내부는 수정할 수 없기 때문에 이와 같은 라이브러리를 사용합니다.

위의 설명과 같이 title 태그를 수정하거나 meta 태그를 추가하고 싶을 때 사용한다.

 

사용법

가장 상위 컴포넌트를 HelmetProvider로 감쌉니다. 

<HelmetProvider>
      <App />
</HelmetProvider>

 

이후 App 컴포넌트 내에서 Helmet 컴포넌트를 사용하여 head 태그를 수정할 수 있습니다.

import { Helmet } from "react-helmet-async";
import "./styles.css";
import InnerComponent from "./InnerComponent";
import { useState } from "react";

export default function App() {
  const [shouldShowInner, setShouldShowInner] = useState(false);

  return (
    <div className="App">
      <Helmet>
        <title>인사말</title>
        <link rel="canonical" href="https://www.tacobell.com/" />
      </Helmet>
      <h1>Hello World</h1>
      <button onClick={() => setShouldShowInner((prev) => !prev)}>
        {shouldShowInner ? "인사말" : "본론으"}로 가기
      </button>
      {shouldShowInner && <InnerComponent />}
    </div>
  );
}

 

컴포넌트 내부 helmet은 안쪽의 컴포넌트부터 우선됩니다.

 

따라서 위와 같은 코드에서는 처음에는 인사말이지만 button을 클릭하면 본론 컴포넌트가 생기면, innerComponet에 있는 Helmet이 우선되어 title이 바뀌게 됩니다. 

'Framework > React' 카테고리의 다른 글

[React] 리덕스(Redux)  (1) 2024.10.07
[React] React Router - BrowserRouter  (0) 2024.10.04
[React] Redux의 configureStore와 Provider  (0) 2024.10.02
[React] React Query의 핵심 객체, QueryClient  (0) 2024.10.02
[React] React ApexCharts  (0) 2024.09.05

Redux 상태 관리 시스템을 React 애플리케이션에 통합하다.

 

index.tsx

import configureStore from './store/configureStore';

const store = configureStore();

root.render(
	<Provider store={store}>
    		<App />
	</Provider>,
);

 

이 코드에서 Provider는 최상위 컴포넌트(App)를 감싸고, 그 하위 컴포넌트들이 Redux 스토어에 접근할 수 있도록 해줍니다.

 

1. configureStore()

  • 설명: configureStore는 Redux Toolkit에서 제공하는 함수로, Redux 스토어를 설정하는 데 사용됩니다. 이 함수는 기본적으로 여러 가지 설정을 간편하게 해주며, 미들웨어나 Redux DevTools를 자동으로 설정합니다.
  • 주요 특징:
    • 간편한 설정: redux-thunk와 같은 미들웨어가 기본적으로 포함되어 있어 비동기 액션을 쉽게 처리할 수 있습니다.
    • Redux DevTools: 브라우저의 Redux DevTools 확장을 자동으로 활성화해 상태 변경을 시각적으로 확인 가능.
    • 미들웨어 설정: 추가적인 미들웨어를 설정하거나 기본 설정을 재정의할 수 있습니다.

 

2. <Provider store={store}>

  • 설명: Provider는 react-redux 라이브러리에서 제공하는 컴포넌트로, React 애플리케이션에 Redux 스토어를 주입하는 역할을 합니다. store는 configureStore로 생성된 Redux 스토어입니다.
  • 주요 역할:
    • Redux 스토어를 React 컴포넌트에 전달: React 애플리케이션의 모든 하위 컴포넌트들이 Redux 스토어에 접근할 수 있도록 해줍니다.
    • Context API 사용: Provider는 내부적으로 Context API를 사용하여 하위 컴포넌트에서 useSelector와 useDispatch를 통해 스토어의 상태를 읽고, 액션을 디스패치할 수 있게 합니다.

 

 

./store/configureStore.ts

import { legacy_createStore as createStore } from 'redux';

import rootReducer from './reducers';

const configureStore = (): any => createStore(rootReducer);

export default configureStore;

 

이 코드는 Redux의 스토어를 설정하는 함수 configureStore를 정의한 것으로, 전역 상태 관리를 위해 createStore를 사용하여 Redux 스토어를 생성하는 역할을 합니다. 

 

1. import { legacy_createStore as createStore } from 'redux';

  • 설명: legacy_createStore는 Redux Toolkit에서 제공하는 함수로, 기존의 createStore를 대체하는 방식입니다. redux의 최신 버전에서는 configureStore를 사용하는 것이 권장되지만, 이 코드에서는 기존 방식인 createStore를 사용하고 있습니다. legacy_createStore로 불리는 이유는 Redux Toolkit이 권장되면서 기존의 createStore가 레거시로 간주되기 때문입니다.
  • 주요 역할: Redux 스토어를 생성하고 관리하는 핵심 함수로, 애플리케이션의 상태 트리를 관리합니다.

2. import rootReducer from './reducers';

  • 설명: rootReducer는 애플리케이션에서 사용하는 리듀서(reducer)들의 집합입니다. Redux의 리듀서는 상태와 액션을 기반으로 새로운 상태를 반환하는 순수 함수입니다.
  • rootReducer의 역할: 여러 개의 리듀서를 combineReducers로 결합하여 전체 애플리케이션의 상태를 관리합니다. rootReducer는 이를 종합하여 Redux 스토어에서 사용할 수 있는 형태로 제공됩니다.

 

리듀서(reducer)들의 집합

 

./store/reducers.ts

import { combineReducers } from 'redux';

import main from '../redux/reducers/mainReducer';
.
.
.
.
.

const rootReducer = combineReducers({
  main,
  app,
  tenant,
  login,
  mybenefit,
  receipt,
  snowpoint,
  user,
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

 

  • rootReducer: 이 코드는 Redux의 combineReducers를 사용하여 여러 개의 리듀서를 결합한 것이며, rootReducer는 애플리케이션의 전체 상태 트리를 관리하는 함수가 됩니다. 리듀서들은 상태를 관리하는 역할을 하므로, rootReducer가 반환하는 것은 애플리케이션의 전체 상태입니다.
  • typeof rootReducer: TypeScript에서 typeof를 사용하면, 해당 변수나 함수의 타입을 가져올 수 있습니다. 여기서는 rootReducer 함수의 타입을 가져옵니다.
  • 결과: ReturnType<typeof rootReducer>는 rootReducer 함수가 반환하는 전체 상태 트리의 타입을 추론합니다. 즉, RootState 타입은 rootReducer에서 반환되는 상태 객체의 구조를 기반으로 자동으로 추론됩니다. 이제 이 RootState 타입을 사용하면 Redux의 상태에 접근할 때 타입 검사를 통해 오류를 방지할 수 있습니다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false, // refetch 조건 막기
      retry: 0, // 요청 실패했을 경우 재요청 횟수
    },
  },
});

 

QueryClient

  • 설명: QueryClient는 React Query의 핵심 객체로, 서버 데이터를 가져오고 관리하는 역할을 합니다. 모든 쿼리는 이 QueryClient 객체를 통해 관리되며, 다양한 옵션을 설정할 수 있습니다.

defaultOptions

  • 설명: defaultOptions는 모든 쿼리에서 공통으로 적용될 기본 옵션들을 설정하는 부분입니다. 여기에서 쿼리 관련 기본 동작을 정의할 수 있습니다.

queries

  • 설명: queries는 모든 쿼리에 적용되는 기본 설정을 정의합니다. 이 설정은 개별 쿼리에서 별도로 정의하지 않는 한 전역으로 적용됩니다.

 

1. refetchOnWindowFocus: false

  • 설명: 브라우저 창에 포커스가 다시 돌아왔을 때 데이터를 다시 가져오는 동작을 막는 설정입니다.
  • 기본 동작: React Query는 기본적으로 사용자가 브라우저 탭이나 창에 다시 포커스를 맞출 때(다시 활성화될 때) 자동으로 데이터를 다시 가져옵니다. 그러나 이 옵션을 false로 설정하면 이러한 자동 갱신을 방지합니다.
  • 사용 이유: 애플리케이션에서 빈번하게 서버 데이터를 다시 불러올 필요가 없거나, 사용자가 창을 전환해도 데이터를 다시 가져오는 것이 불필요한 경우.

2. retry: 0

  • 설명: 서버 요청이 실패했을 때 재시도할 횟수를 설정합니다. 여기서는 retry: 0으로 설정되어 있으므로, 요청이 실패하면 재시도를 하지 않고 즉시 실패로 처리됩니다.
  • 기본 동작: React Query는 기본적으로 서버 요청이 실패했을 때 3번까지 자동으로 재시도합니다. 하지만 이 설정을 통해 재시도를 하지 않도록 설정한 것입니다.
  • 사용 이유: 서버 요청 실패 시 재시도를 하지 않고, 즉각적으로 에러를 처리하고 싶을 때 사용됩니다. 예를 들어, 네트워크 트래픽을 줄이거나, 사용자가 즉시 실패를 인식해야 하는 경우에 유용합니다.

 

전체적으로

  • 이 설정은 창이 다시 활성화되어도 데이터를 다시 가져오지 않으며, 서버 요청 실패 시 추가 재시도를 하지 않도록 구성된 QueryClient를 생성하는 예시입니다. 이는 사용자가 데이터를 자주 다시 불러오지 않아도 되고, 서버 요청 실패 시 자동 재시도가 필요 없는 경우에 적합한 설정입니다.

 

React ApexCharts 사용법 정리

ApexCharts는 다양한 차트를 손쉽게 그릴 수 있는 라이브러리입니다. 이를 React에서 사용하기 위해 react-apexcharts 패키지를 활용합니다. 아래는 react-apexcharts 사용법을 소개하는 자료입니다.

1. 설치

먼저, 프로젝트에 **react-apexcharts**와 **apexcharts**를 설치해야 합니다.

npm install react-apexcharts apexcharts

 

2. 기본 구성

ApexCharts를 React에서 사용하기 위한 기본적인 구조는 아래와 같습니다. series와 options는 차트의 데이터 및 옵션을 지정하는 중요한 속성입니다.

import React from 'react';
import ReactApexChart from 'react-apexcharts';
import { ApexOptions } from 'apexcharts';

const ChartComponent = () => {
  const series = [
    {
      name: "Series 1",
      data: [10, 20, 30, 40],
    }
  ];

  const options: ApexOptions = {
    chart: {
      type: 'bar',
    },
    xaxis: {
      categories: ['A', 'B', 'C', 'D'],
    },
  };

  return (
    <ReactApexChart options={options} series={series} type="bar" height={350} />
  );
};

export default ChartComponent;

 

3. 주요 속성

3.1. type

차트의 유형을 결정합니다. 아래와 같은 차트 유형을 사용할 수 있습니다:

  • line, bar, area, pie, donut, radar 등.
<ReactApexChart type="bar" />

 

3.2. series

차트에 표시될 데이터를 나타냅니다. 각 series는 하나의 데이터 집합을 나타내며, 이름과 값이 필요합니다.

const series = [
  { name: "Data Set 1", data: [30, 40, 35] },
  { name: "Data Set 2", data: [20, 30, 25] }
];
 

3.3. options

차트의 설정을 정의하는 옵션 객체입니다. x축, y축, 툴팁, 그래프 스타일 등을 정의할 수 있습니다.

const options: ApexOptions = {
  chart: {
    type: 'bar'
  },
  xaxis: {
    categories: ['Category 1', 'Category 2', 'Category 3']
  },
  colors: ['#F44336', '#E91E63'],
};

 

4. 실습 예시: DoubleBarChart

다음은 react-apexcharts를 사용하여 막대 그래프 2개를 그리는 예시입니다.

4.1. 컴포넌트 코드

import React, { useState } from 'react';
import ReactApexChart from 'react-apexcharts';
import { initDoubleBarChartOptions } from '../../initSeries';

interface DoubleBarChartProp {
  height: 300 | 350 | 400;
  colors: string[];
  annotations?: any;
  categories?: any;
  valueFormat?: 'default' | 'toFixed';
  series: { name: string; data: number[] }[];
}

const DoubleBarChart: React.FC<DoubleBarChartProp> = ({
  height,
  colors,
  annotations,
  categories,
  valueFormat,
  series,
}) => {
  const [state, setState] = useState({ series });
  const [options, setOptions] = useState(
    initDoubleBarChartOptions(
      height,
      colors,
      annotations,
      categories,
      valueFormat,
    ),
  );

  return (
    <div className="mb-2">
      <ReactApexChart
        options={options}
        series={state.series}
        type="bar"
        height={height}
      />
    </div>
  );
};

export default DoubleBarChart;

 

4.2. 데이터 바인딩 및 로딩 처리

아래 코드는 실시간 데이터를 불러와서 DoubleBarChart에 적용하는 예시입니다.

import { useEffect, useState } from 'react';
import DoubleBarChart from '../../components/Charts/DoubleBarChart';
import SeriesIsLoading from '../../components/Charts/SeriesLoading';
import { useRecoilValue } from 'recoil';
import { loggedInUserState } from '../../store/loggedInUserAtom';
import { fetchData } from '../../api';

const TodayIotStatusContentsWrapper = () => {
  const loggedInUser = useRecoilValue(loggedInUserState);
  const [seriesIsLoading, setSeriesIsLoading] = useState(false);
  const [series, setSeries] = useState([]);

  useEffect(() => {
    const getIotTodayHistory = async () => {
      try {
        const response = await fetchData(
          `/iot/history/realtime/${loggedInUser?.companyId}`,
        );
        setSeries([
          { name: 'NORMAL', data: response.data.normal },
          { name: 'ERROR', data: response.data.error },
        ]);
      } catch (error) {
        console.error(error);
      }
    };

    getIotTodayHistory();
  }, [loggedInUser]);

  return (
    <>
      {seriesIsLoading ? (
        <SeriesIsLoading />
      ) : (
        <DoubleBarChart
          height={350}
          colors={['#3C50E0', '#80CAEE']}
          series={series}
        />
      )}
    </>
  );
};

export default TodayIotStatusContentsWrapper;

 

5. 마무리

  • ReactApexChart는 다양한 차트 형식을 제공하며, 데이터를 series에 바인딩하고, 차트 설정은 options에서 관리합니다.
  • useEffect를 통해 데이터를 비동기로 가져와 그래프를 업데이트할 수 있습니다.
  • 다양한 차트 타입과 옵션을 제공하여, 필요에 맞는 그래프를 쉽게 만들 수 있습니다.

+ Recent posts