이 페이지는 PageTurner AI로 번역되었습니다(베타). 프로젝트 공식 승인을 받지 않았습니다. 오류를 발견하셨나요? 문제 신고 →
문제 해결
여기서는 자주 발생하는 문제들과 해결 방법을 공유합니다. 예제는 React를 사용하지만, 다른 것을 사용하더라도 유용하게 참고할 수 있습니다.
액션을 디스패치해도 아무 반응이 없을 때
액션을 디스패치했는데 뷰가 업데이트되지 않는 경우가 있습니다. 왜 이런 일이 발생할까요? 여러 가지 이유가 있을 수 있습니다.
리듀서 인자 변경 금지
Redux가 전달해주는 state나 action을 수정하고 싶은 유혹이 들 수 있습니다. 절대 그렇게 하지 마세요!
Redux는 리듀서에서 전달받은 객체를 변경하지 않는다고 가정합니다. 반드시 매번 새로운 상태 객체를 반환해야 합니다. Immer 같은 라이브러리를 사용하지 않더라도, 상태 변경(mutation)을 완전히 피해야 합니다.
불변성(Immutability) 덕분에 react-redux가 상태의 세분화된 업데이트를 효율적으로 구독할 수 있습니다. 또한 redux-devtools의 시간 여행(time travel) 같은 훌륭한 개발자 경험 기능도 가능해집니다.
예를 들어, 다음과 같은 리듀서는 상태를 변경하므로 잘못된 예시입니다:
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
// Wrong! This mutates state
state.push({
text: action.text,
completed: false
})
return state
case 'COMPLETE_TODO':
// Wrong! This mutates state[action.index].
state[action.index].completed = true
return state
default:
return state
}
}
다음과 같이 재작성해야 합니다:
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
// Return a new array
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
// Return a new array
return state.map((todo, index) => {
if (index === action.index) {
// Copy the object before mutating
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
코드 양은 더 많지만, 이 방식 덕분에 Redux가 예측 가능하고 효율적으로 동작합니다. 코드 양을 줄이고 싶다면, React.addons.update 같은 도우미를 사용해 간결한 문법으로 불변성 변환을 작성할 수 있습니다:
// Before:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
// After
return update(state, {
[action.index]: {
completed: {
$set: true
}
}
})
객체를 업데이트하려면 Underscore의 _.extend 같은 도구나, 더 나은 방법으로 Object.assign 폴리필을 사용해야 합니다.
Object.assign을 올바르게 사용하세요. 예를 들어, 리듀서에서 Object.assign(state, newData) 같은 형태로 반환하는 대신 Object.assign({}, state, newData)를 반환하세요. 이렇게 하면 이전 state를 덮어쓰지 않습니다.
더 간결한 문법을 위해 객체 전개 연산자 제안을 사용할 수도 있습니다:
// Before:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
// After:
return state.map((todo, index) => {
if (index === action.index) {
return { ...todo, completed: true }
}
return todo
})
실험적인 언어 기능은 변경될 수 있음을 유의하세요.
또한 깊은 복사가 필요한 중첩 상태 객체에도 주의하세요. _.extend와 Object.assign 모두 상태의 얕은 복사본(shallow copy)을 만듭니다. 중첩 상태 객체 처리 방법은 중첩 객체 업데이트를 참고하세요.
dispatch(action) 호출 잊지 않기
액션 생성자를 정의했다고 해서, 호출만으로 액션이 자동으로 디스패치되지는 않습니다. 예를 들어 다음 코드는 아무 동작도 하지 않습니다:
TodoActions.js
export function addTodo(text) {
return { type: 'ADD_TODO', text }
}
AddTodo.js
import React, { Component } from 'react'
import { addTodo } from './TodoActions'
class AddTodo extends Component {
handleClick() {
// Won't work!
addTodo('Fix the issue')
}
render() {
return <button onClick={() => this.handleClick()}>Add</button>
}
}
동작하지 않는 이유는 액션 생성자가 단순히 액션을 반환하는 함수이기 때문입니다. 실제로 디스패치하는 것은 여러분의 몫입니다. 서버에서 렌더링하는 앱은 요청마다 별도의 Redux 스토어가 필요하므로, 정의 시점에 특정 스토어 인스턴스에 액션 생성자를 바인딩할 수 없습니다.
해결 방법은 스토어 인스턴스의 dispatch() 메서드를 호출하는 것입니다:
handleClick() {
// Works! (but you need to grab store somehow)
store.dispatch(addTodo('Fix the issue'))
}
컴포넌트 계층 구조 깊은 곳에 있다면 스토어를 수동으로 전달하기 번거롭습니다. 그래서 react-redux는 connect 고차 컴포넌트를 통해 Redux 스토어에 구독하는 것 외에도 컴포넌트 props에 dispatch를 주입해줍니다.
수정된 코드는 다음과 같습니다:
AddTodo.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { addTodo } from './TodoActions'
class AddTodo extends Component {
handleClick() {
// Works!
this.props.dispatch(addTodo('Fix the issue'))
}
render() {
return <button onClick={() => this.handleClick()}>Add</button>
}
}
// In addition to the state, `connect` puts `dispatch` in our props.
export default connect()(AddTodo)
필요하다면 dispatch를 다른 컴포넌트에 수동으로 전달할 수도 있습니다.
mapStateToProps가 올바른지 확인하세요
액션을 올바르게 디스패치하고 리듀서를 적용하고 있음에도 해당 상태가 props로 올바르게 전달되지 않을 수 있습니다.
다른 문제가 발생하는 경우
#redux Reactiflux Discord 채널에서 문의하거나 이슈를 생성하세요.
문제를 해결했다면 동일한 문제를 겪는 다른 개발자를 위해 이 문서를 수정해주세요.