let user = createSlice({
name : 'user' ,
initialState : { name : 'kim' , age: 20},
}
이렇게 state 가 object나 array 자료인 경우에 state 변경하는 법에 대해 공부했다.
reducers : {
changeName(state){
return { name : 'park' , age: 20}
// return 하고 새로운 값을 넣어주면 이 값으로 기존 state를 갈아치워 준다.
// 여기서 기존 state 의 값이 필요한 경우 파라미터 하나 추가
},
여기서 return하면 기존 state 를 갈아치워 주니까 이렇게 해도 되지만
무슨 말이냐면 객체를 다시 리턴하지 않고, 그 안의 내용물을 직접 건드려도 바뀐다!!
reducers : {
changeName(state){
state.name = 'park';
// state : 기존 state
},
// 이건 immer.js라는 라이브러리가 자동으로 state 복사본을 리턴해 줘서 가능한 거라고 함. (아래에 # 설명)
그래서 버튼을 누르면 age가 변경되는 걸 만들어보려고 했다.
<button onClick={()=>{
state.user.age = 30;
}}> 버튼 </button>
이렇게 해봤는데 안됨.
function으로 담아서 dispatch 해야만 바뀌는 건가?
일단 해봤는데
store.js
reducers : {
changeName(state){
state.name = 'park';
},
changeAge(state){
state.age++;
}
export let { changeAge } = user.actions;
Cart.js
import { changeAge } from "./../store.js";
<button onClick={()=>{
dispatch(changeAge());
}}> 버튼 </button>
이렇게 하니까 된다.
저건 꼭 store.js에 있는 slice 안의 함수 안에 담아서 해야 되는 건가? 그냥 하면 안 되나? 라는 의문이 들었다.
그러니까 store.js의 slice 안에 함수를 만들어서 그것을 export 하고 import 하는 수고를 거쳐 usedispatch() 안에 감싸서 사용해야 한다는 건데 왜 그렇게까지? 라는 생각이 들어서...
... 여기까지 쓰고 보니까, 이거 그냥 state 가져다 쓰는 방법이랑 똑같잖아!!!!
다른 점이 없었다.!!! 왜 다르다고 생각했지?
네, 맞아요! 🎉 당신이 깨달은 내용은 Redux의 상태 관리 원칙을 정확히 이해한 것이에요. 아주 훌륭합니다! 👏
Redux에서는 모든 state(object, array, primitive 값 등등 상관없이)를 변경할 때 반드시 Reducer를 통해야 합니다. 이는 Redux의 핵심 설계 철학 중 하나로, 상태 변화의 예측 가능성과 변화의 추적성을 보장하기 위함이에요. 그러니, state가 단순한 문자열이나 숫자 같은 primitive 값이든, 복잡한 객체나 배열이든, 반드시 Slice의 reducers에 상태 변경 로직을 정의하고 dispatch를 통해 호출해야 합니다.
Redux에서 state를 직접 변경하면 안 되는 이유
어떤 state든 Reducer를 통해서 변경해야 한다
예시:
let user = createSlice({
name: 'user',
initialState: 'kim',
reducers: {
changeName(state) {
return 'park';
}
}
});
위처럼 단순한 문자열 상태를 변경할 때도, 반드시 Reducer를 만들어야만 상태를 안전하게 업데이트할 수 있습니다.
Reducers와 Dispatch를 사용하는 방식
reducers에 상태를 변경하는 로직을 정의합니다.reducers에 정의된 함수는 export된 뒤, 필요한 컴포넌트에서 dispatch로 호출됩니다.dispatch를 통해 상태를 변경해야 Redux가 이 상태 변경을 감지하고, React 컴포넌트를 업데이트합니다.사용 예시:
import { useDispatch, useSelector } from "react-redux";
import { changeName } from "./store.js";
function MyComponent() {
const userName = useSelector((state) => state.user);
const dispatch = useDispatch();
return (
<div>
<h1>{userName}</h1>
<button onClick={() => dispatch(changeName())}>
이름 변경
</button>
</div>
);
}
중요한 점:
dispatch는 Redux에게 액션을 전달하는 역할을 합니다. Redux는 이 액션을 기반으로 어떤 Reducer를 실행할지 결정하고, 상태를 업데이트합니다.dispatch를 사용하지 않고 직접 state를 수정하려고 하면 Redux의 상태 관리 흐름을 깨뜨리게 되고, 제대로 작동하지 않습니다."Redux에서는 모든 state는 반드시 Slice의 reducers 안에서 관리해야 하며, 변경할 때는 reducers의 함수를 dispatch를 통해 호출해야 한다."
=> 정확히 맞습니다! 👌 Primitive state, Object state, Array state 모두 이 규칙이 동일하게 적용됩니다.
Redux의 이 규칙은 단순히 "귀찮은 절차"가 아니라, 다음과 같은 장점을 제공합니다:
- 예측 가능성: 상태 변화의 모든 과정을 추적할 수 있어 디버깅이 쉽습니다.
- 중앙 집중화된 관리: 모든 상태 변경이 명확히 정의된 Reducer를 통해 이루어지므로, 상태 관리가 체계적이고 유지보수가 용이합니다.
- 확장성: 규모가 커져도 일관된 방식으로 상태를 관리할 수 있습니다.
깨달음을 얻음...!!
그리고 이전 코드가 안 되는 이유도 물어봤다.
좋은 질문이에요! Redux와 React의 작동 방식을 이해하는 데 매우 중요한 부분을 짚으셨어요. 질문을 중심으로 하나하나 설명드릴게요. 😊
<button onClick={() => {
state.user.age = 30;
}}>버튼</button>
위 코드는 오류가 나는 이유를 한마디로 정리하자면, Redux의 불변성(immutability) 원칙을 위반했기 때문이에요.
Redux에서는 state를 직접 수정해서는 안 된다는 규칙이 있습니다. 이유는 Redux가 변경된 state를 감지하기 위해 이전 상태와 새로운 상태를 비교(참조 비교)하기 때문이에요. 만약 state를 직접 수정해버리면 Redux는 상태가 변경되었는지 알 수 없게 됩니다.
state.user.age = 30; // 이렇게 직접 수정하면 Redux는 감지하지 못합니다.
Redux에서는 새로운 state 객체를 만들어서 반환해야 합니다. 예를 들어:
return { ...state, user: { ...state.user, age: 30 } };
// 기존 객체를 복사한 뒤 변경 사항을 적용하는 방식
이렇게 해야 Redux가 이전 상태와 새로운 상태를 비교하고 변경된 부분을 감지해 컴포넌트를 다시 렌더링할 수 있어요.
changeName(state) {
state.name = 'park';
}
이 코드는 왜 정상적으로 동작하느냐 하면, Redux Toolkit에 포함된 Immer라는 라이브러리 덕분이에요. Immer는 마치 state를 직접 변경하는 것처럼 코드를 작성할 수 있도록 도와줍니다.
Immer는 내부적으로 불변성을 유지하면서 새로운 state를 생성합니다. 즉, 위 코드는 실제로는 아래처럼 작동합니다:
changeName(state) {
return { ...state, name: 'park' }; // 불변성을 유지하며 새로운 객체를 반환
}
따라서 Redux Toolkit을 사용할 때는 state를 직접 변경하는 것처럼 작성해도 괜찮은 거예요. 하지만 이것은 store.js에 있는 reducer 함수 내부에서만 가능합니다. 컴포넌트에서 직접 state를 변경하려고 하면 불가능합니다.
Redux에서는 상태(state)를 변경할 때 반드시 Reducer를 통해야 합니다. 이 이유는 Redux의 설계 원칙 중 하나인 "모든 상태 변화는 명시적이어야 한다"를 지키기 위함입니다.
상태 관리의 예측 가능성
Redux의 구조를 따르는 규칙
dispatch(action) -> reducer -> state 업데이트라는 흐름을 따릅니다. 이 구조를 벗어나면 Redux가 상태 변경을 감지할 수 없습니다.불변성 보장
네, Redux에서는 상태를 변경할 때 반드시 Slice에서 만든 Reducer 함수(또는 직접 정의한 Reducer)를 통해야 합니다. 그렇지 않으면 Redux는 상태 변경을 인지하지 못하고, React 컴포넌트가 변경된 상태로 다시 렌더링되지 않습니다.
import { useDispatch } from "react-redux";
import { changeName } from "./store.js";
function MyComponent() {
let dispatch = useDispatch();
return (
<button onClick={() => {
dispatch(changeName());
}}>이름 변경</button>
);
}
위 코드는 Redux의 상태를 변경하는 올바른 방식입니다. dispatch를 통해서만 상태를 변경해야 Redux가 이를 감지하고 React 컴포넌트를 업데이트합니다.
왜 직접 state를 바꾸면 안 되나요?
왜 Slice 안에서 함수(reducer)를 만들어야 하나요?
왜 Slice 안의 reducer 함수는 state를 직접 변경하는 것처럼 보이지만 동작하나요?
Immer를 사용하기 때문에 직접 변경하는 것처럼 코드를 작성해도 새로운 상태를 반환하도록 처리해줍니다.컴포넌트에서 state를 변경하려면 어떻게 해야 하나요?
dispatch 함수를 사용해서 Reducer를 호출해야 합니다.+ 또 생긴 궁금증 : 함수로 만들면 된다면 익명함수로도 될까? dispatch 안에다가 () => {} 이렇게해서 전달하면 어떻게 되지?
<button onClick={()=>{
dispatch(()=>{ state.user.age ++ })
}}> 버튼 </button>
이렇게 해봤더니 에러가 뜬다.

Cannot assign to read only property 'age' of object '#<Object>'
읽기 전용 데이터를 바꿀 수 없어서 오류가 난다고 한다.
좋은 질문이에요! 😊 Redux에서 상태를 변경할 때 왜 익명 함수가 바로 작동하지 않는지, 그리고 Redux의 reducers에서 익명 함수를 사용하면 어떻게 되는지 자세히 설명드릴게요.
dispatch로 익명 함수를 직접 전달했을 때 발생한 오류당신이 작성한 코드는 아래와 같습니다:
<button onClick={() => {
dispatch(() => {
state.user.age++;
});
}}>
버튼
</button>
이 코드가 동작하지 않는 이유는 Redux의 상태 관리 방식과 관련이 있습니다:
state.user.age++는 Redux의 상태를 직접 변경하려고 시도한 것입니다.state를 불변 객체로 다루며, 상태를 직접 변경하지 않고 새로운 상태를 반환해야만 Redux가 이를 감지하고 정상적으로 작동합니다.dispatch가 기대하는 것은 무엇일까?dispatch는 단순히 아무 함수나 받을 수 있는 것이 아닙니다. Redux는 다음 두 가지를 기대합니다:
dispatch는 일반적으로 { type: 'action_type', payload: some_data } 형식의 Action 객체를 받습니다. 이 객체는 Reducer가 어떤 동작을 수행해야 할지 알려주는 역할을 합니다.dispatch가 함수도 받을 수 있습니다. 하지만 이 함수는 특정 미들웨어를 통해 처리된 뒤 Action 객체를 반환하거나 추가적인 동작을 수행해야 합니다.익명 함수는 Redux의 상태를 직접 변경하려는 시도를 포함하기 때문에 동작하지 않습니다. Redux에서는 상태를 변경하는 로직을 Reducer 함수 안에 정의해야만 합니다.
reducers에 상태를 변경하는 함수를 정의하고, 이를 dispatch로 호출합니다:
let user = createSlice({
name: 'user',
initialState: { name: 'kim', age: 20 },
reducers: {
incrementAge(state) {
state.age++; // Immer 라이브러리가 상태를 안전하게 업데이트합니다.
},
},
});
export const { incrementAge } = user.actions;
export default user.reducer;
컴포넌트에서 사용하는 방법:
import { useDispatch } from "react-redux";
import { incrementAge } from "./store";
<button onClick={() => {
dispatch(incrementAge());
}}>
버튼
</button>
위처럼 incrementAge는 Reducer 함수 안에 정의된 함수이므로, Redux가 이를 적절히 처리하고 상태를 안전하게 업데이트할 수 있습니다.
reducers 안에 익명 함수를 작성하면 어떻게 될까?let user = createSlice({
name: 'user',
initialState: { name: 'kim', age: 20 },
reducers: {
() => {
console.log('익명 함수입니다!');
},
},
});
위 코드는 동작하지 않습니다. Slice의 reducers 안에 있는 함수는 단순히 익명 함수가 아니라, Reducer 함수여야 합니다. Reducer 함수는 현재 상태(state)를 받아 새로운 상태를 반환해야 하기 때문에, 다음과 같은 기본 구조를 따라야 합니다:
(state, action) => {
// 상태를 안전하게 변경하는 로직
return newState;
}
dispatch는 익명 함수를 처리하지 않습니다.
reducers 안에 Reducer 함수를 정의한 뒤, 이를 dispatch로 호출해야 합니다.reducers 안에서도 익명 함수는 사용할 수 없습니다.
reducers는 상태를 변경하는 로직을 정의하는 곳이며, 각 함수는 특정 형식을 따라야 합니다. 익명 함수는 이 형식을 따르지 않기 때문에 사용할 수 없습니다.Reducer는 상태를 "직접 변경하지 않고" 항상 새로운 상태를 반환해야 합니다.
Immer 라이브러리를 사용해 불변성을 유지하며, Reducer 함수 내부에서 안전하게 상태를 변경할 수 있습니다.let user = createSlice({
name: 'user',
initialState: { name: 'kim', age: 20 },
reducers: {
changeName(state) {
state.name = 'park';
},
incrementAge(state) {
state.age++;
},
},
});
export const { changeName, incrementAge } = user.actions;
export default user.reducer;
컴포넌트에서:
import { useDispatch } from "react-redux";
import { changeName, incrementAge } from "./store";
function App() {
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(changeName())}>이름 변경</button>
<button onClick={() => dispatch(incrementAge())}>나이 증가</button>
</div>
);
}
이렇게 작성하면 Redux의 상태 변경 흐름에 맞게 안전하게 작동합니다.
그리고 계속 언급되는 immer.js 에 대해서도 궁금해져서 물어봤다.
아주 좋은 질문이에요! 😊 immer.js는 Redux에서 중요한 역할을 합니다. Redux의 상태 관리와 불변성(immutability)을 유지하면서도 개발자 경험을 개선해주는 핵심 도구입니다. 이 질문에 대해 하나씩 정리해서 설명드릴게요.
immer.js란 무엇인가요?immer.js는 JavaScript의 객체와 배열을 불변(immutable)하게 다루는 것을 더 쉽게 만들어주는 라이브러리입니다. Redux Toolkit에서 기본적으로 포함되어 있으며, createSlice를 사용할 때 내부적으로 자동으로 사용됩니다.
기본적으로 Redux에서는 상태(state)를 변경할 때 불변성을 유지해야 합니다. 상태를 직접 수정하면 안 되고, 항상 새로운 객체를 만들어 반환해야 하죠. 이 작업이 반복되면 코드가 장황하고 복잡해질 수 있습니다. 이때 immer.js가 개발자를 도와줍니다.
Redux는 상태(state)를 변경하기 위해 순수 함수인 Reducer를 사용합니다. Reducer는 이전 상태와 Action을 받아 새로운 상태를 반환합니다. Redux에서 불변성을 유지하는 이유는 다음과 같습니다:
변화를 감지하기 위해
Redux는 상태가 변경되었는지 확인하기 위해 참조 비교(Reference Equality)를 사용합니다. 참조 비교는 객체의 내용이 아닌 메모리 주소를 비교합니다.
// 예시
const state1 = { name: "kim" };
const state2 = { name: "kim" };
console.log(state1 === state2); // false (새로운 객체)
참조가 변경되었을 때만 Redux는 상태가 업데이트되었음을 감지하고, 컴포넌트가 리렌더링됩니다.
상태의 예측 가능성을 보장하기 위해
상태를 직접 변경하면 예기치 않은 부작용이 발생할 가능성이 있습니다. 불변성을 유지하면 상태 변경이 항상 "새로운 상태"를 생성하므로, Redux 애플리케이션의 상태 추적과 디버깅이 쉬워집니다.
불변성을 지키지 않는 코드:
function reducer(state, action) {
// 직접 상태를 변경
state.name = "park";
return state; // 같은 객체를 반환
}
이 코드는 Redux의 동작 원리에 어긋납니다. Redux는 참조 비교를 통해 상태가 변경되었는지 확인하는데, 위 코드는 같은 객체를 반환하므로 상태가 변경되지 않았다고 판단합니다. 따라서 상태 업데이트가 작동하지 않을 수 있습니다.
immer.js가 Redux에서 하는 역할immer.js는 개발자가 Redux의 불변성을 신경 쓰지 않아도 상태를 "안전하게" 변경할 수 있도록 도와줍니다. 구체적으로:
Draft 객체를 제공
immer는 상태를 가상 초안(Draft)으로 만들어 줍니다. 초안 객체는 마치 가변 객체처럼 동작하지만, 실제로는 불변성을 유지합니다.
즉, 초안 상태를 자유롭게 수정하면, immer가 내부적으로 이를 기반으로 새로운 상태를 생성합니다.
직접 변경하는 것처럼 작성 가능
상태를 불변 객체로 유지하면서도, 가변 객체를 다루듯 코드를 작성할 수 있습니다.
예를 들어, 아래 두 코드는 동일한 결과를 반환합니다.
immer.js 없이:
function reducer(state, action) {
return {
...state,
name: "park",
};
}
immer.js 사용:
function reducer(state, action) {
state.name = "park"; // 불변성을 유지하며 새로운 상태 반환
}
immer 덕분에 상태를 직접 수정하는 것처럼 보이지만, 실제로는 새로운 상태 객체를 만들어 반환합니다.
immer.js의 사용Redux Toolkit은 기본적으로 immer.js를 내장하고 있습니다. 예를 들어 createSlice로 Reducer를 작성할 때, 우리는 state를 직접 변경하는 것처럼 작성해도 문제가 없습니다.
const user = createSlice({
name: "user",
initialState: { name: "kim", age: 20 },
reducers: {
changeName(state) {
state.name = "park"; // Immer가 내부적으로 불변성을 유지해줌
},
incrementAge(state) {
state.age += 1; // Immer 덕분에 직접 수정 가능
},
},
});
위 코드는 내부적으로 아래처럼 동작합니다:
function changeName(state) {
return {
...state,
name: "park", // 새로운 객체를 반환
};
}
function incrementAge(state) {
return {
...state,
age: state.age + 1,
};
}
immer가 불변 객체를 생성해주기 때문에, 개발자가 번거롭게 새로운 객체를 만들어 반환할 필요가 없습니다.
만약 불변성을 깨뜨리려고 하면 오류가 발생하거나, Redux가 상태 변경을 감지하지 못할 수 있습니다. immer는 이러한 문제를 방지해줍니다.
예를 들어, 다음과 같은 코드:
function reducer(state, action) {
state.name = "park"; // 불변성을 깨뜨림
return state;
}
위 코드는 immer를 사용하면 작동하지만, 사용하지 않으면 Redux가 상태 변경을 감지하지 못해 문제가 발생합니다.
immer.js를 사용하지 않고도 상태를 변경할 수 있을까?물론 가능합니다. 하지만 불변성을 유지하기 위해 항상 새로운 객체를 반환해야 하므로 코드가 복잡해질 수 있습니다.
function reducer(state, action) {
return {
...state,
name: "park", // 상태를 복사하고 새로운 값을 추가
};
}
이 방식은 불변성을 유지하지만, 코드가 장황하고 실수할 가능성이 커집니다. 특히 상태가 깊은 중첩 구조를 가질 경우 코드가 훨씬 복잡해질 수 있습니다.
immer.js는 Redux에서 불변성을 유지하면서도 상태를 쉽게 변경할 수 있도록 도와주는 도구입니다. 주요 이점은:
createSlice를 사용할 때 자동으로 immer가 적용됩니다.immer.js 덕분에 Redux 상태 관리가 훨씬 간편해졌고, 실수할 가능성도 줄어들었습니다. 하지만 불변성의 중요성을 이해하는 것은 여전히 Redux를 잘 활용하는 데 중요한 개념입니다. 😊