치춘짱베리굿나이스
[프리온보딩] 220515 강의 메모 02 (코드 예시) 본문
코드 예시
리덕스로 다크모드 구현하기
전역 상태관리 라이브러리
- 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
에서 가져온theme
을color-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
훅을 사용하면setData
나useMount
를 이용한 처리도 할 필요 없다- 설정 가능한 값의 종류가 매우 다양하기 때문에 리액트 쿼리가 어렵다
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
) 를Suspense
와ErrorBoundary
로 감싸준다
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
'프로젝트 > 원티드 프리온보딩' 카테고리의 다른 글
[프리온보딩] 220517 그룹과제 #2 (2) | 2022.05.18 |
---|---|
[프리온보딩] 220517 강의 메모 01 (코드리뷰) (0) | 2022.05.17 |
[프리온보딩] 220515 강의 메모 01 (코드리뷰) (0) | 2022.05.17 |
[프리온보딩] 220516 그룹과제 #2 시작 (0) | 2022.05.17 |
[프리온보딩] 220510 강의 메모 02 (날씨앱 + 팁) (0) | 2022.05.17 |
Comments