React에서 state는 컴포넌트 안에 있다. 근데 여러 컴포넌트가 같은 데이터를 써야 하면?
App
├── Header ← 여기서도 로그인 정보 필요
├── Sidebar ← 여기서도 로그인 정보 필요
└── Main
└── Profile ← 여기서도 로그인 정보 필요
Redux는 state를 컴포넌트 밖, Store라는 창고에 모아둔다. 모든 컴포넌트가 여기서 꺼내 쓴다.
Store의 state는 직접 바꾸면 안 된다. 반드시
dispatch → Reducer를 통해서만 바꿔야 한다.
버튼 클릭 → dispatch(액션) → Reducer → 새 state → 화면 업데이트
combineReducers로 합친다.앱 최상단에서 딱 한 번 만들어진다.
// index.js
const store = createStore(rootReducer, applyMiddleware(logger, thunk));
ReactDOM.render(
<Provider store={store}> // 전체 앱을 감싸서 Store를 공급
<App />
</Provider>,
document.getElementById('root')
);
combineReducers로 역할별 Reducer를 하나로 합친다:
// modules/index.js
const rootReducer = combineReducers({
counter, // 카운터 담당
sample, // post, users 담당
loading, // 로딩 상태 담당
});
dispatch는 기본적으로 객체(액션) 만 받는다. 즉시 Reducer로 보내버린다.
근데 "1초 뒤에 실행", "API 응답 오면 실행" 같은 걸 하려면 끼워넣을 공간이 없다.
미들웨어는 dispatch와 Reducer 사이에 자리를 만들어준다.
dispatch(액션) → logger → thunk → Reducer → 새 state
미들웨어는 Store를 만들 때 등록한다:
const store = createStore(
rootReducer,
applyMiddleware(logger, thunk) // 순서대로 통과
);
실무에서는 redux-logger 라이브러리를 쓰면 된다.
여기서는 미들웨어가 내부적으로 어떻게 동작하는지 이해하기 위해 직접 만들어봤다.
미들웨어 구조는 함수가 3겹으로 중첩된 형태다. Redux가 store → next → action 순서로 하나씩 넘겨주는 구조에 맞춰진 것이다.
const loggerMiddleware = (store) => (next) => (action) => {
// store : Redux가 처음에 넘겨줌 (상태 조회용)
// next : Redux가 두 번째로 넘겨줌 (다음 미들웨어 or Reducer로 넘기는 함수)
// action : 실제 dispatch될 때 넘겨줌
console.log("이전 상태", store.getState()); // 아직 안 바뀜
console.log("액션", action);
next(action); // ← 없으면 Reducer에 도달 안 함!
console.log("다음 상태", store.getState()); // 바뀐 후
};
next(action)을 호출해야 다음 미들웨어 또는 Reducer로 넘어간다. 이게 없으면 액션이 거기서 멈춘다.
dispatch가 호출될 때 thunk 미들웨어가 이걸 확인한다: "넘어온 게 함수야, 객체야?"
// thunk 내부 동작 (개념)
if (typeof action === "function") {
action(dispatch); // 함수면 → thunk가 직접 실행시켜줌
} else {
next(action); // 객체면 → 그냥 Reducer로 보냄
}
평소엔 dispatch({ type: "INCREASE" }) 처럼 객체를 넘기면 Reducer로 직행한다.
근데 함수를 넘기면 thunk가 가로채서 실행시켜준다. 덕분에 아래와 같이 가능해진다.
// counter.js — increaseAsync를 풀어서 읽으면
export const increaseAsync = () => { // 1. increaseAsync() 호출하면
return (dispatch) => { // 2. 함수를 반환 (thunk가 이걸 실행)
setTimeout(() => {
dispatch(increase()); // 3. 1초 뒤에 dispatch
}, 1000);
}
};
useEffect — 컴포넌트가 화면에 나타났을 때 실행할 것을 지정한다.
useEffect(() => {
getPost(1); // 화면 뜨자마자 API 호출
getUsers(1);
}, []); // [] = 처음 화면 떴을 때 딱 한 번만 실행
두 번째 인자에 따라 실행 시점이 달라진다:
useEffect(() => {...}) — 매 렌더링마다 실행useEffect(() => {...}, []) — 처음 한 번만 실행useEffect(() => {...}, [count]) — count가 바뀔 때마다 실행connect — Redux Store와 컴포넌트를 연결한다. 두 가지 일을 한다:
export default connect(
({ sample, loading }) => ({
post: sample.post, // Store에서 꺼내서 props로
loadingPost: loading["sample/GET_POST"],
}),
{ getPost, getUsers }, // dispatch 연결
)(SampleContainer);
1. 화면 뜸
→ useEffect 실행 → getPost(1) 호출
2. dispatch(thunk 함수)
→ logger: 콘솔에 찍음
→ thunk: "함수네!" → 실행시켜줌
→ loading: true → 로딩 스피너 표시
3. API 호출 (axios)
→ 서버 응답 대기 중...
4. dispatch({ type: GET_POST_SUCCESS, payload: data })
→ Reducer가 state 업데이트
→ loading: false → 로딩 스피너 사라짐
5. connect가 변화 감지
→ 새 state를 props로 전달
→ 화면 리렌더링 → 데이터 표시 🎉


React 앱을 빌드하면 모든 JS가 하나의 파일로 합쳐진다.
앱이 커질수록 사용자가 첫 접속 시 받아야 하는 파일이 무거워진다.
한 번도 방문하지 않는 페이지 코드까지 미리 다운로드하는 건 낭비다.
코드 스플리팅: 필요한 코드만 그때그때 로드해서 초기 로딩 속도를 높인다.
import notify from "./notify"; // 앱 시작할 때 무조건 다운로드

버튼을 한 번도 안 눌러도 notify.js는 이미 다운로드 돼 있다. 낭비.
const onClick = () => {
import("./notify").then((result) => result.default());
};


버튼을 클릭하는 그 순간 처음으로 notify.js를 다운로드한다.
import()는 Promise를 반환하므로 .then()으로 완료 후 처리한다.
컴포넌트를 스플리팅할 때 사용한다.
const SplitMe = React.lazy(() => import("./SplitMe"));
React.lazy는 이 컴포넌트가 화면에 처음 나타날 때 그제야 다운로드한다.
다운로드가 끝나기 전까지 React는 "아직 준비 안 됨" 상태다. 이때 Suspense가 대신 fallback UI를 보여준다.
// 식당에 앉자마자 음식 나오기 전 물 먼저 주는 것과 같다
<Suspense fallback={<div>loading...</div>}>
{visible && <SplitMe />}
</Suspense>
단점: SSR 미지원, Suspense로 반드시 감싸야 함.
3단계의 단점을 보완한 라이브러리.
const SplitMe = loadable(() => import("./SplitMe"), {
fallback: <div>loading...</div>,
});
fallback을 옵션으로 바로 넘김preload() 기능 제공// 마우스 올리는 순간 미리 다운로드 → 클릭 시 즉각 표시
const onMouseOver = () => {
SplitMe.preload();
};
preload: 사용자가 마우스를 올리는 순간 파일을 미리 받아놓는다. 실제 클릭 시 이미 로드되어 있어 즉각 표시 → UX 향상.
| 단계 | 방식 | 특징 |
|---|---|---|
| 1단계 | 정적 import | 무조건 미리 다운로드 (낭비) |
| 2단계 | 동적 import | 클릭 시 다운로드, 함수/모듈용 |
| 3단계 | React.lazy | 클릭 시 다운로드, 컴포넌트용, Suspense 필요 |
| 4단계 | loadable | SSR 지원 + preload 가능, 실무 권장 |