치춘짱베리굿나이스

[프리온보딩] 220515 강의 메모 02 (코드 예시) 본문

프로젝트/원티드 프리온보딩

[프리온보딩] 220515 강의 메모 02 (코드 예시)

치춘 2022. 5. 17. 19:48

코드 예시

리덕스로 다크모드 구현하기

전역 상태관리 라이브러리

  • context api : 비추함
    • 속도가 느림
    • 사용할 때마다 코드가 캐스캐이딩되어서 코드가 금방 더러워진다
  • redux toolkit : 추천함
    • 바닐라 redux에 여러 기능과 라이브러리를 포함해서 쉽게 쓸 수 있도록 되어있음
    • mutation 관리, Thunk 관리, 캐싱 등
    • 일반 리덕스보다 훨씬 쓰기 편함
  • recoil
    • 아직도 알파단계라 뭐가 금방금방 바뀜
    • atom 정도 (+ selector) 만 쓸 것

redux 사전 설정

import { Provider } from 'react-redux';
import { store } from './states'; // 전역 상태값 저장하는 store 위치
...
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
    <React.StrictMode>
        <Provider store={store}>
            <BrowserRouter>
                <Routes />
            </BrowserRouter>
        </Provider>
    </React.StrictMode>
);
  • 리덕스로 전역 상태를 관리하기 위해 Provider로 앱을 감싸주자
  • 보통 앱 컴포넌트는 Routes의 하위로 들어가기 때문에 BrowserRouter를 감싸주면 된다

redux toolkit을 이용한 전역 상태값 정의하기

import store from 'store';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

import type { RootState } from '.';

export interface SystemState {
    theme: string;
}; // 테마 타입 지정

const INITIAL_STATE: SystemState = {
    theme: store.get('testApp.theme') || 'light',
}; // 로컬 스토리지에서 값 가져오기 시도하고 없으면 light로 지정

const systemSlice = createSlice({
    name: 'system',
    initialState: INITIAL_STATE,
    reducers: {
        setTheme: (state: SystemState, action: PayloadAction<string>) => {
            state.theme = action.payload
        }, // 테마를 지정할 수 있는 setTheme 함수
    },
});

export const { setTheme } = systemSlice.actions;

export default systemSlice.reducer;

export const getTheme = (state: RootState): string => state.system.theme;
// 테마를 가져올 수 있는 getTheme
export interface TodoState {
    todoList: string;
};

const INITIAL_STATE: SystemState = {
    todoList: store.get('testApp.localTodo') || INIT_TODO,
};

const systemSlice = createSlice({
    name: 'system',
    initialState: INITIAL_STATE,
    reducers: {
        setTodoList: (state: SystemState, action: PayloadAction<string>) => {
            state.todoList = action.payload;
        },
    },
});

export const { setTodoList } = systemSlice.actions;

export default systemSlice.reducer;

export const getTodoList = (state: RootState): string => state.todo.todoList;
// state.[index에서 지정한 리듀서 이름].todoList
  • src/states 내에 위와 같이 전역 상태값을 지정한다
  • 초기값을 initialState로 지정하고, 밖에서 사용할 함수 get, set 을 export 한다
import { configureStore } from '@reduxjs/toolkit';

import system from './system';
import todo from './todo';

export const store = configureStore({
    reducer: {
        system,
        todo,
    },
    devTools: process.env.NODE_ENV !== 'production', // 배포 단계에서 데브툴 끄기
    middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false }),
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
  • src/states 내의 index.js를 위와 같이 설정한다
  • system, todo 에서 정의한 상태값을 store에 모아준다

useAppDispatch, useAppSelector

import { useDispatch } from 'react-redux';
import type { AppDispatch } from 'states/index';

export const useAppDispatch = (): AppDispatch => useDispatch<AppDispatch>()
  • Redux 공식 문서 참고하기
  • useDispatch, useSelector를 타입스크립트 특성상 타입때문에 한번 래핑해서 사용해야 한다

redux toolkit을 이용한 상태값 사용하기

import { useAppSelector } from 'hooks'; // get함수 가져올 때 필요
import { getTodoList } from 'states/todo';

const TodoList = () => {
    const todoList = useAppSelector(getTodoList);
import { useAppDispatch } from 'hooks'; // set함수 가져올 때 필요
import { setTodoList } from 'states/todo';

const TodoItem = ({ todo }: Props) => {
    const dispatch = useAppDispatch(setTodoList);
  • 값만 가져올 땐 Selector, 값을 수정하고자 할 땐 dispatch를 가져온다
dispatch(setTodoList(
    todoList.map((item) => (
        item.id === Number(id) ? { ...item, done: checked } : item
    )
)));
  • 전역 상태값을 수정하고 싶으면 이렇게 사용한다

reducer 추가하기

...
const systemSlice = createSlice({
    name: 'system',
    initialState: INITIAL_STATE,
    reducers: {
        setTheme: (state: SystemState, action: PayloadAction<string>) => {
            const newColorSet = action.payload;
            store.set('testApp.theme', newColorSet);
            state.theme = newColorSet;
        }, // 테마를 지정할 수 있는 setTheme 함수
        toggleTheme: (state: SystemState) => {
            const newColorSet = state.theme === 'light' ? 'dark' : 'light';
            store.set('testApp.theme', newColorSet);
            state.theme = newColorSet;
        },
    },
});
...
export const { setTheme, toggleTheme } = systemSlice.actions;
...
  • state를 직접 넣어서 수정할 수도 있지만, state를 수정하는 함수를 리듀서에 추가할 수도 있다
  • 위처럼 작성하면 굳이 dispatch 안에서 삼항연산자를 쓸 필요 없이 toggleTheme을 호출하면 된다
  • 임시로 값을 newColorSet에 저장하고, 이로 state.theme도 수정하고 로컬스토리지에도 저장할 수 있다

redux toolkit을 이용한 테마 설정하기

import { getTheme, setTheme } from 'states/system';

const GNB = () => { // 내비게이션 바
    ...
    const dispatch = useAppDispatch(setTheme);
    const theme = useAppSelector(getTheme);
    ...

    const handleThemeClick = () => {
        dispatch(setTheme(theme === 'light' ? 'dark' : 'light'));
    };

    const handleThemeClick = () => {
        dispatch(toggleTheme());
    };
  • setTheme을 사용하면 인자로 어떤 문자열로 theme을 바꿀 것인지 넣어주면 되고, toggleTheme을 사용하면 인자 없이 가능하다

설정한 테마로 전역 색상 바꾸기

...
const systemSlice = createSlice({
    name: 'system',
    initialState: INITIAL_STATE,
    reducers: {
        setTheme: (state: SystemState, action: PayloadAction<string>) => {
            const newColorSet = action.payload;
            store.set('testApp.theme', newColorSet);
            document.documentElement.setAttribute('color-theme', newColorSet);
            state.theme = newColorSet;
        }, // 테마를 지정할 수 있는 setTheme 함수
        toggleTheme: (state: SystemState) => {
            const newColorSet = state.theme === 'light' ? 'dark' : 'light';
            store.set('testApp.theme', newColorSet);
            document.documentElement.setAttribute('color-theme', newColorSet);
            state.theme = newColorSet;
        },
    },
});
...
export const { setTheme, toggleTheme } = systemSlice.actions;
...
...
import { getTheme } from 'states/system';
import { useAppSelector } from 'hooks';
...

const App = () => {
    const theme = useAppSelector(getTheme);

    useMount(() => {
        document.documentElement.setAttribute('color-theme', theme);
    });
  • 위처럼 설정하면 App (최상위 컴포넌트) 이 마운트되었을 때 useAppSelector를 통해 전역 store에서 가져온 themecolor-theme 속성으로 지정할 수 있다
<html lang='en' color-theme='light'>
...
  • 위처럼 속성을 지정해줄 경우 최상위 태그인 html의 attribute가 바뀐다
  • document.documentElement는 문서의 최상위 루트 요소를 나타내는 Element를 반환하므로, 최상위 루트에 위치한 <html> 태그에 attribute가 지정되는 것
:root[color-theme: 'dark'] & {
    background-color: black;

    h1 {
        color: white;
    }
}
  • html 태그가 root에 해당하므로, css에서 :root 선택자를 통해 html 태그의 attribute를 가져올 수 있다
  • color-theme attribute를 이용하여 전역 색상을 설정하자

리액트 쿼리로 날씨 데이터 받아오기

react-query 사전 설정

import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
...

const queryClient = new QueryClient({
    defaultOptions: { queries: { refetchOnMount: false } }.
});

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
    <React.StrictMode>
        <QueryClientProvider client={queryClient}>
            <ReactQueryDevtools initialIsOpen /> {/* 리액트쿼리용 개발 툴*/}
            <Provider store={store}>
                <BrowserRouter>
                    <Routes />
                </BrowserRouter>
            </Provider>
        </QueryClientProvider>
    </React.StrictMode>
);
  • redux 쓰듯이 최상위 컴포넌트를 감싸주자
  • ReactQueryDevtools는 개발 단계에서 왼쪽 구석에 나오는 툴로, 이걸 사용해서 리액트 쿼리로 받아온 데이터를 볼 수 있다
  • 당연히 배포 단계에서는 꺼야 한다

react-query로 데이터 받아보기

const WeatherApp = () => {
    const lat = 35.1231314;
    const lon = 139.124124113; // 위도, 경도 데이터

    const { data, isLoading, refetch } = useQuery(
        ['getWeatherForecast5daysApi', lat, lon],
        () => getWeatherForecast5daysApi({ lat, lon }).then((res) => res.data),
        {
            cacheTime: 3000, // 캐시 유지 시간 (설정한 시간 내에는 재요청 안함)
            refetchInterval: 3000, // 재요청 텀
            refetchOnWindowFocus: false, // 다른 탭 또는 창으로 갔다가 해당 페이지로 돌아올 때마다 fetch할지
            onError(err) {
                if (isAxiosError(err)) console.log(err);
            }, // 에러 발생 시 행동
            // 설정값
        },
    );
    ...
    const handleSubmit = () => {
        refetch();
    };
    ...
    <form onSubmit={handleSubmit}>
        ...
    </form>
}
  • useQuery의 첫 번째 인자는
    • getWeatherForecast5daysApi: 키 값, useQuery에서 고유한 값을 갖는다
    • lat, lon: 위도, 경도값이므로 같은 위도, 경도에 대해 재요청 없이 캐시를 사용하도록 한다
    • 첫 번째 인자로 들어온 배열 내의 중 하나라도 값이 바뀌면 재요청을 시도한다
  • 두 번째 인자는 데이터를 실질적으로 받아오는 함수이며, Promise 객체를 반환하는 함수가 들어간다
  • 세 번째 인자는 리액트 쿼리 설정값으로, 캐시 유지 시간이나 데이터 불러오는 조건, 오류 발생 시 재요청 횟수 등을 지정가능하다
    • onSuccess, onError에 함수를 지정하여 성공, 실패 여부에 따라 다른 동작을 지정할 수 있다
  • 반환값
    • data는 받아온 데이터
    • isLoading은 로딩 여부를 나타내며 이를 이용하여 로딩 화면을 보여줄 수 있다
    • refetch 함수는 호출하면 바로 데이터를 새로 불러온다
  • useQuery 훅을 사용하면 setDatauseMount 를 이용한 처리도 할 필요 없다
  • 설정 가능한 값의 종류가 매우 다양하기 때문에 리액트 쿼리가 어렵다

react-query와 Suspense, ErrorBoundary 사용하기

const Loading = () => {
    return (
        <div classame={styles.loading}>
            Loading...
        </div>
    );
};
const Error = ({ error }: Props) => {
    return (
        <div className={styles.error}>
            {error.message}
        </div>
    );
};
  • 로딩 컴포넌트, 에러 컴포넌트를 만들자
<Suspense fallback={<Loading />}>
    <ErrorBoundary fallbackRender={({ error }) => <Error error={error} />}>
        <Weather />
    </ErrorBoundary>
</Suspense>
  • react-query로 데이터를 받아오는 컴포넌트 (위에서는 Weather) 를 SuspenseErrorBoundary로 감싸준다
const { data, isLoading, refetch } = useQuery(
    ['getWeatherForecast5daysApi', lat, lon],
    () => getWeatherForecast5daysApi({ lat, lon }).then((res) => res.data),
    {
        ...
        useErrorBoundary: true,
    },
);
  • react query 설정에서 useErrorBoundary 속성을 true로 하면 Error Boundary를 사용할 수 있다

기타 팁

  • 문자열 (string) 에 1 곱해서 Number 형식으로 형변환하는 것보단 차라리 Number() 로 감싸서 형변환하는 것이 안전하다
  • 화면 스타일 작업할 땐 작은 걸 먼저 작업하고 크게 늘리는 것이 효율적이고 에러날 확률이 적다
    • 큰 디자인을 작게 줄이려고 시도할 때 보통 문제가 많이 생긴다
    • 디자이너한테도 모바일 먼저 작업하고 PC로 늘리는 것이 훨씬 쉬울 것이다
    • 코드의 양도 훨씬 줄어들고, 웹페이지를 열 때 데이터 소모량도 감소함
    • Mobile first code
  • CSS Grid Layout ⇒ 그리드 형식으로 페이지 레이아웃 구성하기 좋음 (좀 어려움)
  • 리액트 쿼리 + 리코일이 궁합이 좋다
  • 리덕스 툴킷으로는 상태관리
  • rem이 작업하기엔 제일 나은데, 폰트크기 비례라 정확한 수치를 알기 어려워 디자이너들이 매우 싫어한다
    • rem, px 같은 단위는 디자이너들의 요구에 맞추면 된다
  • !important 사용하는 이유: 우선순위가 높은 다른 선택자를 막아버리기 위함
  • Debouncing, Throttling
  • Axios Cancel Token
Comments