완성된 예발자닷컴에 놀러오세요! 😊
리액트 컴포넌트의 계층구조는 데이터 관리에 있어 때로는 비효율적다. 뎁스가 깊은 하위 컴포넌트에 필요한 데이터가 있을때 실제로 그 데이터를 사용하지 않는 컴포넌트를 거쳐가야하기 때문이다.
사실 예발자 프로젝트는 앱 규모가 복잡하지 않고, 동적으로 state값이 변할 일이 없기 때문에 굳이 상태관리 라이브러리가 필요 없을수도 있겠지만 편리함을 경험해보자는 취지로 redux
를 사용해보기로 했다.
redux가 배우기 쉽지 않은 개념이었는데 백을 담당하신 secho님께서 프론트 팀보다 먼저 정말 열심히 공부하시고 우리 프로젝트에 바로 적용할 수 있는 정도까지의 사용법을 공유해주셨다. 덕분에 리덕스를 너무 잘 이해한 것 같아서 감사할따름.. 이 글은 secho님께 배운 내용을 정리한 글이다.
Store
프로젝트당 1개만 등록한다. 모든 state와 reducer를 스토어에 모아서 관리한다.
Reducer
컴포넌트의 상태값이 변하면 action을 전달받은 reducer 함수가 호출되고 action을 참고하여 새로운 state를 반환한다.
Action
리덕스에서는 프로젝트에서 상태변화를 일으키는 것을 하나의 action 이라고 본다. 구체적으로 action은 type
값, 바뀐 data
를 가지고 있는 객체이다. type은 문자열이며, 어떤 값을 변화시킬지 정해 reducer에게 알려주는 역할을 한다.
Dispatch
action을 매개변수로 받아 reducer를 호출한다.
액션생성함수 (Action Creator)
type을 결정하고 action
을 반환하는 함수.
더 자세한 내용은 여기 확인
redux
폴더를 만들고 그 안에 actions
, reducers
, store
폴더를 만든다.
// redux/store/configureStore.js
import { applyMiddleware, compose } from 'redux';
import { createWrapper} from 'next-redux-wrapper';
import { createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import { combineReducers } from 'redux';
import { createLogger } from 'redux-logger';
import thunk from 'redux-thunk';
import reducer from '../reducers/index';
import reviewReducer from '../reducers/reviewReducer';
const configureStore = () => {
const logger = createLogger();
const middlewares = [thunk , logger];
//배포용과 개발용의 미들웨어 차이를 두기 위함
const enhancer = process.env.NODE_ENV === 'production'
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(thunk))
const store = createStore(
combineReducers({
reducer,
reviewReducer}), enhancer);
return store;
};
store/configureStore.js
파일에 스토어를 정의한다.
몇 가지 유용한 리덕스 라이브러리들을 추가해줬는데 하나씩 알아보자면,
createWrapper
컴포넌트에 store 등록하는 과정을 편하게 해준다.
composeWithDevTools
state 변화를 한 눈에 확인할 수 있는 Redux DevTolls
라는 크롬 확장프로그램을 우리 프로젝트에 사용할 수 있게 해준다.
combineReducers
sub reducer 들를 하나로 합쳐줌으로서 여러 리듀서를 한 store에 등록할 수 있게 해준다.
createLogger
store가 바뀔때마다 로깅해주는 미들웨어. 리덕스 관련 정보들을 콘솔에서 예쁘게 확인할 수 있다.
thunk
json
데이터를 불러올 때 비동기 처리를 하는데, 비동기 처리를 가능하게 해주는 미들웨어. 이 미들웨어를 사용하면 액션(객체)이 아닌 함수를 디스패치 할 수 있다.
이제 reducer를 만들때마다 store에 추가하면서 관리하면 된다.
원래 store를 사용하려면 provider
로 컴포넌트를 감싸줘야되는데, _app.js
에 wrapper
를 쓰면 일일이 컴포넌트를 감싸주지 않아도 된다.
configureStore.js
파일 하단에 아래 코드를 추가해주자.
// redux/store/configureStore.js
const wrapper = createWrapper(configureStore, {
debug: process.env.NODE_ENV === 'development',
});
export default wrapper;
// redux/reducers/reviewReducer.js
const initialState = {
data: {}
};
컴포넌트 별로 리듀서의 초기 state값을 빈 객체로 만들어준다. 여러 리듀서를 결합해서 store를 만들기 때문에 store.state
를 찍으면 각 컴포넌트 리듀서들의 초기값이 모두 들어있는 걸 확인할 수 있다. 이래서 중앙관리인가보다.
// redux/reducers/reviewReducer.js
import { HYDRATE } from 'next-redux-wrapper';
const initialState = {
data: {}
};
const reviewReducer = (state = initialState, action) => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload };
case 'GET_REVIEW_DATA':
return { ...state, data: action.payload };
default:
return state;
}
};
export default reviewReducer;
초기 state값을 등록했으면 액션을 받아 state를 바꿀 수 있도록 switch/case 문을 만들어준다.
아직 액션생성함수는 만들지 않았지만 'GET_REVIEW_DATA'
type을 가진 액션이 dispatch를 통해 전달되면, state 즉 reviewReducer.data가 action.payload.data 로 바뀌도록 reducer를 만들었다.
이제 액션생성함수를 만들어 type
과 payload
를 가진 액션 객체를 정의해주자.
// redux/actions/reviewAction.js
import axios from 'axios';
export const getData = async () => {
const reviewData = await axios.get('http://localhost:5000/api/json/reviews');
return {
type: 'GET_REVIEW_DATA',
payload : reviewData.data
}
}
axios를 사용해 백 서버에 올라와있는 json 파일을 불러오는 액션생성함수를 만든다. 이 함수는 type과
바꿀 데이터(payload
)를 지정한 액션 객체를 반환한다.
axios를 사용하면 서버 데이터만 가져오는 게 아니라 header 등등 여러 정보들을 함께 가져온다. 따라서 payload에는 reviewData.data
를 담아줘야 우리에게 필요한 json 데이터를 가져올 수 있다.
엑시오스를 사용하면 Network Error가 난다. CORS(교차출처리소스공유) 정책에 의한 오류이다. 허가되지 않는 곳에서 리소스를 요청하면 보안상 위협이 되기 때문에 요청을 받는 곳에서 CORS를 허용해줘야한다.
CORS 패키지를 인스톨 한 뒤 json.js 에서 cors('http://localhost:3000')
를 추가해주자.
router.get('/reviews', cors('http://localhost:3000'), function(req, res, next){
// component/FAQ.js
import React , { useEffect }from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { getData } from '../redux/actions/faqAction';
export default function Review( {program} ) {
const reviewData = useSelector((state) => state.reviewReducer.data);
const dispatch = useDispatch();
useEffect(()=>{
getData().then(function(result){
dispatch(result)
});
},[]);
const programData = reviewData[program] ?? [];
return (
<>
<div className={styles.box}>
{programData?.map((v, idx, id) =>
<CommentList data={v} key={idx} id={idx}/>)}
</div>
</>
);
}
useSelector
를 통해 state
값을 가져올 수 있는데, reviewData
에 처음에는 빈 객체(아까 만든 초기값)이 들어갈 것이다. 왜냐면 dispatch
함수를 써야 action
이 발생하고 그래야 reducer
가 호출되어 state
값을 변화시킬 수 있기 때문이다.
그래서 dispatch는 useEffect 함수 안에 넣어줘야 한다. useEffect은 비동기적으로 리액트 앱의 렌더링이 끝난 뒤 가장 마지막에 실행되는 리액트 내장함수이기 때문이다.
즉, Review 컴포넌트는 아주 빠른시간에 두 번 렌더링 된다. 상태변화가 일어나고 액션이 발생하면 리렌더링을 통해 바뀐 state를 갖게 되는 것이다.
dispatch를 구체적으로 어떻게 사용하는지, 코드를 살펴보자.
const dispatch = useDispatch();
useEffect(() => {
getData().then(function(result){
dispatch(result)
});
},[]);
dispatch
는 reducer
를 호출한다. 따라서 action
객체를 매개변수로 받는다.
action
을 생성하기위해 getData()
함수를 호출한다.
getData() 함수 비동기처리를 기다리기 위해 then
메서드와 콜백함수
를 사용한다. 만약 위 코드에서 콜백함수를 사용하지 않는다면 데이터를 온전히 받아오지 못해 우리가 참조해 사용할 수 없게 된다. 자바스크립트 비동기처리의 문제점을 해결하고 싶다면 꼭 콜백함수를 사용하자.
result
에는 getData()의 반환값이 담긴다. 이를 dispatch에 전달해 reducer를 호출한다!
이제 reviewData
에는 바뀐 state, 즉 우리에게 필요한 서버로부터 전달받은 데이터가 담기게 된다. 원래 프론트에서 만들었던 더미데이터처럼 마음껏 참조해 사용할 수 있는 것이다.
사실 이때 문제가 발생한다. 리뷰 컴포넌트가 두 번 렌더링 된다면 처음 렌더링 시점에서는 reviewData[program]
에 아무 값도 들어있지 않기 때문에 값을 참조할 수 없어 렌더링 ERROR가 나기 때문이다.
그래서 이 처음보는... ??
이라는 자바스크립트 문법을 사용해줘야 한다.
const programData = reviewData[program] ?? [];
??
은 왼쪽 피연산자 값이 null
이나 undefined
인 경우 오른쪽 피연산자를 반환한다. 이를 통해 초기 렌더링 시점에서 존재하지 않는 값에 대해 예외처리를 할 수 있다.
마찬가지로 ?.
연산자도 에러 예외처리를 위해 사용했다.
programData?.map()
?.
는 참조한 값이 null
이나 undefined
인 경우 실행을 멈추고 undefined를 리턴하는 연산자이다. programData는 빈 배열이라 map을 사용할 수 없다. 첫 번째 렌더링 시점에서 예외처리를 해주고, 리렌더링 된 두 번째 렌더링부터는 데이터를 참조해 가져와 사용할 수 있게 된다.