Plain Javascript object holds information that influences the output of render
React 공식문서에서는 위와 같이 상태를 정의했다. state
는 컴포넌트를 렌더링하는데 있어 영향을 주는 정보를 지닌 객체로, 컴포넌트 내부에서 관리된다.
애플리케이션에서 보여지는 데이터 혹은 UI/UX(FE)에 필요한 데이터 중 시간에 따라 변할 가능성이 있는 데이터를 state
에 담아 관리한다.
하나의 컴포넌트에서 상태를 관리하는 것은 매우 간단하다. 하지만 여러 컴포넌트에서 같은 상태를 공통적으로 접근하고 공유해야 할 때 효율적인 상태 관리가 필요하다.
Lifting State Up
여러 컴포넌트에서 공유해야 할 상태가 있다면 공통 소유 컴포넌트를 찾아 해당 컴포넌트의 state
로 관리한다. 만약 특정 컴포넌트에서 state
가 필요하다면 props
로 전달한다.
props
로 상태를 전달하는 것이 비효율적이다. props
로 상태를 전달해야 하는 상황이 발생한다. 이 문제를 해결할 수 있는 방법들을 리액트에서 제공한다.
공통 소유 컴포넌트란?
common owner component
계층 구조 내에서 특정state
가 있어야 하는 모든 컴포넌트들의 상위에 있는 하나의 컴포넌트
먼저, 컴포넌트 합성 (Composite
)를 이용하는 방식이다.
props
로 전달될 수 있는 값에 대해 제한이 없다는 것을 이용한 것이다. props
로 전달하는 것이다. 하지만 합성에서는 props
로 상태를 전달하는 것이 아니라 아예 컴포넌트를 전달한다. function App() {
const [state, setState] = useState('state');
return (
<>
<Header state={state} setState={setState} />
<Navbar state={state} setState={setState} />
</>
);
}
function Header({state, setState}) {
return (
<>
// 또 다시 props로 상태와 setter함수 전달
<Logo state={state} />
<Setting setState={setState} />
</>
);
}
// 합성 이용
function App() {
const [state, setState] = useState('state');
return (
<>
<Header
logo={<Logo state={state} />}
setting={<Setting setState={setState} />}
/>
...
</>
);
}
다른 방법으로 리액트는 Context API
를 제공한다. Context API
는 주로 전역 상태를 관리할 때 사용되며 컴포넌트끼리 상태를 props
를 전달하지 않고도 한 번에 원하는 컴포넌트로 상태를 가져올 수 있다. 하지만 성능면에서 문제점이 발생한다.
Redux
와 비교하자면 Redux
는 상태의 특정 값을 컴포넌트에서 의존하게 될 때 해당 값이 바뀔 때에만 리렌더링이 되도록 최적화되어 있다. 그래서 관심사에 맞게 Context
를 분리하여 생성하는 것이 중요하다. 서로 관련 없는 상태라면 같은 Context
에 있으면 안된다.
import React, { createContext, useState, useContext } from "react";
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
function useUser() {
return useContext(UserContext);
}
function UserInfo() {
const { user } = useUser();
if (!user) return <div>사용자 정보가 없습니다.</div>;
return <div>{user.username}</div>;
}
function Authenticate() {
const { setUser } = useUser();
const onClick = () => {
setUser({ username: "velopert" });
};
return <button onClick={onClick}>사용자 인증</button>;
}
export default function App() {
return (
<UserProvider>
<UserInfo />
<Authenticate />
</UserProvider>
);
}
사용자가 사용자 인증 버튼을 누르면 Context의 user
상태가 업데이트되므로 UserInfo
컴포넌트는 당연히 리렌더링이 된다. 하지만 user
상태에 의존하지 않는 Authenticate
컴포넌트도 리렌더링되는 문제가 발생한다. 왜냐하면 Authenticate 컴포넌트가 의존하고 있는 setUser
와 user
가 같은 Context
에 있기 때문이다. 이러한 소규모 애플리케이션의 경우 큰 문제가 되지 않지만 애플리케이션 규모가 커질수록 성능 문제가 발생할 수 있다.
따라서, 위 상황에서 두 개의 Context를 사용해야 한다.
import React, { createContext, useState, useContext } from "react";
const UserContext = createContext(null);
const UserUpdateContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={user}>
<UserUpdateContext.Provider value={setUser}>
{children}
</UserUpdateContext.Provider>
</UserContext.Provider>
);
}
function useUser() {
return useContext(UserContext);
}
function useUserUpdate() {
return useContext(UserUpdateContext);
}
function UserInfo() {
const user = useUser();
if (!user) return <div>사용자 정보가 없습니다.</div>;
return <div>{user.username}</div>;
}
function Authenticate() {
const setUser = useUserUpdate();
const onClick = () => {
setUser({ username: "velopert" });
};
return <button onClick={onClick}>사용자 인증</button>;
}
export default function App() {
return (
<UserProvider>
<UserInfo />
<Authenticate />
</UserProvider>
);
}
user
상태가 업데이트되었을 경우 UserInfo
컴포넌트만 리렌더링된다. 이렇게 업데이트용과 상태용 Context를 분리하는 것이 중요하다.
Redux Toolkit 라이브러리를 사용하면 액션 타입, 액션 생성함수, 초기 상태, 리듀서를 하나의 함수로 편하게 선언할 수 있다.
slice
라고 한다. immer
가 내장되어 있기 때문에 스프레드 연산자를 사용하지 않고 원하는 값을 직접 변경해도 상태의 불변성이 유지되면서 업데이트된다.import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1 // 직접 변경 가능
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
// 액션 생성 함수
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// 리듀서
export default counterSlice.reducer
Selector
는 다음과 같은 상황에서 유용하게 사용된다.
만약 상태의 위치가 변경되면 그에 따라 useSelector
의 selector
함수 인자도 일일이 바꿔야 한다. 하지만 기존에 useSelector
의 selsector
함수를 따로 선언하고 필요할 때 불러와 사용하면 이후 상태의 위치가 바뀌어도 selector
함수만 변경하면 된다.
컴포넌트 리렌더링 최적화를 위해 createSelector
를 통해 Memozied Selector
를 만들어 사용한다. createSelector에는 여러 selector
함수들이 인자로 전달될 수 있다. 만약 첫 번째 selector에서 반환된 값이 변경될 때에만 그 다음 selector를 호출하여 원하는 값을 연산하여 조회한다.
import { createSelector } from '@reduxjs/toolkit'
const todosSelector = (state) => state.todos;
const undoneTodos = createSelector(
todosSelector,
(todos) => todos.filter((todo) => !todo.done)
);
function UndoneTasks() {
const tasks = useSelector(undoneTodos);
// ...
}
위의 상황에서는 useMemo
를 이용하여 최적화할 수 있다.
function UndoneTasks() {
const tasks = useSelector(undoneTodos);
const undoneTasks = useMemo(() => tasks.filter(tasks => !tasks.done), [tasks]);
// ...
}
Hook이 도입되기 전, 리덕스를 사용할 때 컴포넌트를 Presentational Component
와 Container Component
로 구분하여 작성했다.
Presentational Component
: props로 상태를 받아와 온전히 뷰만 담당하는 컴포넌트Container Component
: 리덕스와 연동되어 있는 컴포넌트로, 상태 업데이트 로직이 존재한다.이제는 Hook
을 통해서 상태 관련 로직을 컴포넌트에서 분리시킬 수 있기에 더 이상 컴포넌트의 구분이 불필요해졌다. 리덕스의 상태와 액션을 사용하여 상태 로직을 Custom Hook에서 작성하면 된다. 컴포넌트에서는 Custom Hook을 이용하여 UI에만 집중하면 된다.
미들웨어는 다음과 같은 상황에 유용하게 사용된다.
1. 요청을 연달아서 여러번 하게 될 때 이전 요청은 무시하도록 하고 맨 마지막의 요청만 처리하도록 할 때 (Ex. react-saga의 takeLastest
)
2. 특정 조건이 만족되었을 때 이전에 시작한 요청을 취소하는 경우
3. 특정 콜백 함수를 원하는 액션이 디스패치 되었을 때 호출하도록 등록할 때
참고