돔의 레이아웃에 맞추어 작업을 해야할 때 사용한다. 예를 들어, 특정 요소에 대해 툴팁을 만든다고 가정해보자. 이때 해당 요소에 들어갈 텍스트가 상황에 딸 다르다면, 그때마다 요소의 크기를 측정해 툴팁의 x, y 좌표를 지정해야 한다. 이러한 경웨 useLayoutEffect를 사용한다면 요소를 렌더링 하기 전 요소의 크기를 측정하고 툴팀의 레이아웃을 지정할 수 있게 된다.
다만 작업을 모두 동기적으로 실행하고 그동안 렌더링도 지연된다는 점에서 무분별하게 사용한다면 프로젝트의 성능이 떨어질 수 있다.
리액트 프레임워크는 JSX 코드를 분석하여 가상 돔을 생성한다. 만약 상태가 변경되면 이전 가상돔과 비교하여 달라진 부분을 감지하여 리렌더링한다.
1. 가상돔이란 무엇이고, 어떻게 작동하는가?
브라우저가 HTML을 파싱할 때 생성하는 DOM과 동일한 형태이다. 다만 가상 돔은 실제 돔을 조작하는 것이 아니라, UI의 변경점을 확인하는 용도이다. 리액트 프로젝트가 실행되면 각 컴포넌트의 JSX 코드를 분석하여 리액트 요소를 생성한다. 이 요소들이 결합되어 가상돔을 구성한다. 상태의 변경이 일어날 시, 이 과정을 반복하여 매번 새로운 가상돔이 생겨난다. 새로운 가상 돔이 생겨난 이후에는 이전의 가상돔과 비교하여 달라진 부분을 파악한다. 해당 부분만 재렌더링 하여, 효율적인 렌더링이 가능케 한다.
기존 redux에서는 상태 관리 로직에서 불변성 관리가 되지 않아, 기존 값을 따로 복사해야 했음
ex) { ...state, name: 홍길동 }
toolkit을 사용할 시 불변성 유지가 되기 때문에, 변경하고 싶은 값만 변경하면 됨
ex) state.name = "홍길동"
개발자 도구 적용 간편
toolkit은 redux-devtools를 따로 설치하지 않고, 설정만 해도 적용
3. 구현 코드
slice 생성
slice는 reducer와 action을 동시에 관리함
초기 상태 값 설정
slice 내부에 action 생성 (state의 변경 시키고 싶은 값만 수정)
slice를 통해 생성된 action 내보내기
// store/slice/infoSlice.js
import { createSlice } from "@reduxjs/toolkit";
// 초기 상태 값 설정
const initialState = { name: "홍길동", age: 20 };
// slice를 통해 action 생성
const infoSlice = createSlice({
name: "counter",
initialState,
reducers: {
// 액션 생성
// state의 변경 시키고 싶은 값만 수정
changeName: (state, action) => {
state.name = action.payload;
},
changeAge: (state, action) => {
state.age = action.payload;
},
},
});
// reducer에 생성한 action export
export const { changeName, changeAge } = infoSlice.actions;
export default infoSlice.reducer;
store 생성
configure 함수 사용
reducer 등록
개발자 도구 사용 설정
// store/store.js
import { configureStore } from "@reduxjs/toolkit";
import infoReducer from "./slice/infoSlice";
// configureStore 함수 사용
// devTools 따로 설치하지 않고 설정만 해도 사용 가능
const store = configureStore({
// 생성한 reducer 등록
reducer: {
info: infoReducer,
},
// 개발자 도구 사용 설정 ON
devTools: true,
});
export default store;
가장 상위 컴포넌트에 store 등록
// main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store/store";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
{/* 작성한 store 적용 */}
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
middleware는 reducer가 action을 처리하기 전에 추가적인 로직을 가능하게 해줌
비동기 작업, 인증 등 추가적인 작업을 처리가능
4. 단점
boilerplate (작성해야 하는 코드가 많음)
redux를 사용하기 위해 기본적으로 작성해야 하는 코드가 많은 편
redux-toolkit 등의 라이브러리를 통해 코드를 줄일 수 있지만, 그럼에도 다른 상태 관리 라이브러리로 보다는 코드가 긴편
비동기 작업 복잡
비동기 작업을 처리하기 위해 Redux Thunk, Redux Saga 등 middleware를 사용할 수 있지만, 이 역시 다른 상태 관리 라이브러리 보다 사용법이 다소 복잡한 편
5. 구현
reducer 설치
npm i redux react-redux redux-devtools-extension
reducer를 작성
초기 상태 값을 작성
reducer 내부에 각 액션에 따라 어떠한 로직을 실행할 지 작성
action을 작성, payload는 action의 parameter를 의미
생성한 reducer와 action을 export
// store/reducers/infoReducer.js
// 기본 상태 값을 설정
const initialState = {
name: "홍길동",
age: 20,
};
// reducer를 작성
// 각 액션에 따라 어떠한 로직을 실행할 지 작성
const infoReducer = (state = initialState, action) => {
switch (action.type) {
case "CHANGENAME":
return { ...state, name: action.payload };
case "CHANGEAGE":
return { ...state, age: action.payload };
default:
return state;
}
};
// 액션 작성
// changeName 액션 작성
// payload는 action에 담긴 parameter를 의미
const changeName = (value) => ({
type: "CHANGENAME",
payload: value,
});
// changeAge 액션 작성
const changeAge = (value) => ({
type: "CHANGEAGE",
payload: value,
});
// 생성한 reducer와 action export
export { infoReducer, changeName, changeAge };
rootReducer 작성
combineReducers를 통해 생성한 reducer를 모두 묶어줌
comibineReducers에서 reducer에 지정한 키를 통해 상태를 불러 올 수 있음 (useSelector 부분 참고)
// store/reducers/rootReducer.js
import { combineReducers } from "redux";
import { infoReducer } from "./infoReducer";
// 생성한 리듀서를 하나로 묶어 줌
const rootReducer = combineReducers({
// state에서 해당 상태를 불러올 때 state.info를 통해 접근할 수 있음
info: infoReducer,
});
export default rootReducer;
store 작성
createStore은 현재 사용을 권장하지 않음. 현재 코드에서는 redux toolkit을 사용하지 않기에 사용
store에 rootReducer 추가
composeWithDevTools()를 적용하여 redux 개발자 도구 사용 가능
import { createStore } from "redux";
import rootReducer from "./reducers/rootReducer";
import { composeWithDevTools } from "redux-devtools-extension";
// store를 생성
// createStore은 현재 권장되지 않는 방법, 앞으로는 toolkit의 configureStore를 사용합시다.
// store에는 rootReducer를 넣어줌
// composeWithDevTools를 넣어 redux 스토어의 변화를 확인할 수 있음
const store = createStore(rootReducer, composeWithDevTools());
export default store;
redux를 사용하고자 하는 컴포넌트 최상단에 store 적용
// main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store/store";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
{/* redux를 이용하고자 하는 컴포넌트 최상단에 store를 적용 */}
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
redux 사용하기
useSelector로 store 상태 불러오기
useDispatch로 dispatch 사용하기
dispatch를 통해 action을 store로 전달하기
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { changeAge, changeName } from "./store/reducers/infoReducer";
function App() {
// useSelector를 통해 store의 상태를 불러올 수 있음
// rootReducer의 할당한 키를 통해 불러올 수 있음
const info = useSelector((state) => state.info);
// useDispatch에 action을 이용하면 상태 변화 로직을 실행할 수 있음
const dispatch = useDispatch();
const [name, setName] = useState("홍길동");
const [age, setAge] = useState(20);
const handleInputName = (e) => {
setName(e.target.value);
};
const handleInputAge = (e) => {
setAge(e.target.value);
};
const submitName = () => {
// action을 dispatch를 통해 store로 전달
// changeName에 해당하는 상태 변화 로직 실행
dispatch(changeName(name));
};
const submitAge = () => {
// action을 dispatch를 통해 store로 전달
// changeAge에 해당하는 상태 변화 로직 실행
dispatch(changeAge(age));
};
return (
<div className="App" style={{ display: "flex", flexDirection: "column" }}>
<div>{`현재 이름은 ${info.name} 입니다.`}</div>
<div>{`현재 나이는 ${info.age}세 입니다.`}</div>
<div>
<label>
이름
<input onChange={handleInputName} />
</label>
<button onClick={submitName}>이름 변경</button>
</div>
<div>
<label>
나이
<input type="number" onChange={handleInputAge} />
</label>
<button onClick={submitAge}>나이 변경</button>
</div>
</div>
);
}
export default App;
컴포넌트가 마운트(등장), 업데이트, 언마운트(삭제)될 때 애니메이션 효과를 적용할 수 있게 해줌
2. 사용법
react-transition-group 라이브러리 설치
npm i react-transition-group
애니메이션 만들기
//index.css
.fade-enter {
// 컴포넌트 마운트 시 애니메이션 시작할때의 css 설정.
transform: translateX(-300px);
}
.fade-enter-active {
// 컴포넌트 마운트 시 애니메이션 끝났을 때의 css 설정.
transform: translateX(0);
transition: transform 300ms ease-in-out;
}
.fade-exit {
// 컴포넌트 언마운트 시 애니메이션 시작할 때의 css 설정.
transform: translateX(0);
}
.fade-exit-active {
// 컴포넌트 언마운트 시 애니메이션 끝났을 때의 css 설정.
transform: translateX(-300px);
transition: transform 300ms ease-in-out;
}
CSSTransition 컴포넌트로 애니메이션 실행할 컴포넌트 감싸주기
import { useState } from "react";
import { CSSTransition } from "react-transition-group";
// 라이브러리에서 불러오기.
function App() {
const [isShow, setIsShow] = useState(false);
// 컴포넌트 렌더링 조건을 담당하는 state 값.
const toggle = () => {
setIsShow((prevState) => !prevState);
};
return (
<div className="App">
<button onClick={toggle}>{isShow ? "Hide" : "Show"}</button>
// 버튼을 눌러서 컴포넌트 렌더링 여부 달라짐.
<CSSTransition
in={isShow}
// 애니메이션의 실행 기준이 되는 상태 값.
timeout={300}
// 애니메이션의 실행시간.
classNames="fade"
// 애니메이션의 class 명
unmountOnExit>
// 애니메이션 실행된 이후 unmount 할 것인지에 대한 설정. 해당 값이 true면 fade-exit 애니메이션이 실행된 이후 컴포넌트가 사라짐.
<div>애니메이션</div>
</CSSTransition>
</div>
);
}
export default App;