오늘은 Redux
를 배운 것들을 활용하여, Todo List 를 만드는 날이다. 사실, Redux 의 공식문서의 Usage With React 부분을 보고 마저 차근차근 응용해 보는 단계이지만, 최대한 개념을 이해했는지를, 그리고 사용할 수 있는지를 Redux 로 상태관리를 하고, React 에 기반한 To Do List 를 만들어보면서 공부하는 단계이기도 하여서 Redux 문서를 읽고 공부하는 부분과는 분리하게 되었다.
React
여태까지는 Redux 의 개념을 배우기만 했을 뿐, React 와 같이 사용하는 방법을 배우진 못했다. 이번 시간부터는 Redux 를 React와 함께 사용해보도록 하자. Redux 는 React 말고도 Angular, Vue, 그리고 Vanilla JS 와 같이 사용할 수도 있다...만, React
나 Deku
와 쿵짝이 "특히" 잘 맞는 것 또한 사실이다. 왜냐면, 이 두 개는 function of state
를 활용하여 UI 를 그리기 때문이다. Redux
도 이와 비슷하게 (순수)함수 + state
의 조합으로 상태관리를 하기에, 저 두 개와 특별히 궁합이 더 잘 맞을 수밖에 없다.
React Binding 이 기본적으로 Redux
에 포함되어있지 않다. 앱을 처음 만들 때는 npx create-react-app my-app --template redux
를 통해, 이미 적용된 앱에는 npm install react-redux
을 통하여 설치해준다. 나같은 경우 밑바닥부터 새로 만들기 위해 npx create-react-app my-app --template redux
명령어를 통해 세팅을 했다.
지금 사용하는 React Bindings for Redux
는 Presentational Component(state 와는 크게 관련이 없이 그저 그려내기만 하는 component) 와 Container Component(state 에 기반하여 변경되는 값에 대한 handling 이 들어가는 component) 를 구분할 것이다(separate presentational component from container component) 이렇게 component 들을 용도에 따라 구분하게 된다면 코드의 재사용성을 높이고, 가독성 또한 높일 수 있다.
우리가 작성하는 대부분의 component 들은 presentational 이다. 그러나 우리는 Redux store 와 연동하기 위해 container component 들도 어느정도는 만들어야 한다. 그렇다고 해서 또 지금 하는 말이 container component 가 우리의 component tree 최상단에 "무조건" 있어야 한다, 이런 건 아니다. 만약에 엄청나게 nested 된 구조에, 셀 수 없는 callback 호출이 일어나는 component tree 가 있다면, container component 하나를 더 넣어주는 것도 필요하다.
container component 를 store.subscribe()
메서드를 통해 만들어 줄 순 있다. 그러나 Redux 공식문서에서는 추천하진 않는다고 한다. 왜냐면, Redux 단에서 매우매우 많은, 하나하나 수작업으로 해 주기는 어려운 수준의 성능 최적화가 이뤄지기 때문이다. 그렇기 때문에 container component 를 직접 수작업으로 만들어주기보다는, React-Redux 에서 제공하는 connect()
메서드를 이용하는 걸 권장한다고 한다.
우리가 Redux 에서 Reducer 를 만들어 줄 때 Hierarchy 를 고려해서 Reducer 를 작성했던 것처럼, 이제 UI 의 Hierarchy 를 잡아줄 때이다. 이 과정은 Redux "에만" 국한된 것이 아니다. React 문서의 Thinking in React 는 이런 UI 상의 Hierarchy 를 잡아주는 과정에 대해서 아주 잘 설명해 주고 있다. 이해가 안 간다면, 저 부분을 읽고 스스로 작성한 12 Concepts of React 의 이 부분을 다시 한 번 읽고올 수 있도록 하자.
우리가 앞으로 만들 건 간단하다. To do 목록들을 보여줄 것이고, 눌렀을 때는 체크 표시(crossed out)가 되면서 완료된 항목이라는 걸 보여줄 것이다. 그리고 사용자가 새로운 todo 항목을 만들 수 있는 영역도 만들어 주어야 하고, toggle 을 만들어서 "모두 보여주기", "완료된 항목만 보여주기", "미완료 항목들만 보여주기" 와 같이 todo 목록들에 filter 를 걸어줘야 할 것이다.
데이터(state) 와는 관계 없는, 보여주기만 하는 component 들을 구상해보자. 공식문서를 보기 전 내가 만든 presentational component 들은 다음과 같다
Todos
: todo 들을 담는 list, Array
형태Todo
: { id, text, isCleared }
의 세 가지 property 로 구성id
: ToDo 항목에 부여되는 고유한 idtext
: ToDo 항목의 내용isCleared
: 완료 여부공식문서에서는 이런 식으로 구상을 하고있다. 내 구성과 차이점이 있다면 onTodoClick
과 같은 메서드까지도 다 구상해놓았다는 점이다.
TodoList
is a list showing visible todos.todos: Array
is an array of todo items with { id, text, completed }
shape.onTodoClick(id: number)
is a callback to invoke when a todo is clicked.Todo
is a single todo item.text: string
is the text to show.completed: boolean
is whether the todo should appear crossed out.onClick()
is a callback to invoke when the todo is clicked.Link
is a link with a callback.onClick()
is a callback to invoke when the link is clicked.Footer
is where we let the user change currently visible todos이 컴포넌트들은 "보여주는 데" 초점을 맞추었다. 그러나 이 컴포넌트들에서는, "어디로부터" 정보가 오는지, 그리고 "어떻게" 그 정보를 바꿀 수 있는지는 알지 못 한다. 그냥 주는 값대로 화면에 그려줄 뿐이다. 이 Todo App에서 Redux 가 아닌 다른 걸 사용할 때도, 해당 컴포넌트들은 똑같은 기능을 할 것이다. 이유는 그저 Presentational Component, 그냥 "그려주기만 하는" 컴포넌트들이기 때문이다.
이제 Redux 와 연결하기 위해, "상태를 담는(container)" 컴포넌트들을 만들 차례이다. 예를 들자면, 우리가 방금 위에서 만든 Presentational Component 인 TodoList
는, VisibleTodoList
와 같이 Redux store 에 착 달라붙어 현재 visibility filter 의 state 를 받아올 수 있는 container container 가 필요하다.
subscribe
, 직역하면 구독이지만 store 에 "구독" 을 한다는 표현은 적절치 못 하다고 생각해서 착 달라붙는 - 받아오는 과 같은 식으로 이해하고 그렇게 작성했다.그리고 visibility filter 의 상태를 바꾸기 위해서는 FilterLink
라는, click 을 했을 때 적절한 action 을 날려주는 Link
를 render 해주는 container component 가 필요하다.
이번에도 공식문서에서 구상한 것들을 참고하자면, 아래와 같다.
VisibleTodoList
filters the todos according to the current visibility filter and renders a TodoList
.FilterLink
gets the current visibility filter and renders a Link
.filter: string
is the visibility filter it represents.Container component 도 아니고, Presentational Component 도 아닌 무언가가 가끔 존재한다. function 과 form 이 따로 가지 않고 같이 가야하는, 우리로 치면 사용자가 Todo 를 입력하는 걸 받고, 그걸 return 해줄 수 있는 component 같은 걸 의미한다.
공식문서에서는 이를 AddTodo
라는, presentational 도 아니고 container 도 아닌 "other" 로 따로 분류했다
AddTodo
is an input field with an “Add” button기술적으로 우리는 container 인지, 혹은 presentational 인지를 분류해 왔으나 이렇게 작은 건 뭐 괜찮다. 만약에 이 AddTodo 라는 component 의 스케일이 커진다면, 그 때가 되어서 또 나눠도 늦지 않을 듯 하다.
이제 우리가 구상한 컴포넌트들을 한 번 작성해보자. 순서는 Presentational, 그리고 Container. Thinking in React 에서 권장한 순서대로 진행해 보자.
아직은 Redux 를 사용하지 않아도 된다. Redux 가 주는 값을 받을 때 그냥 그 값을 잘 받아 화면에 그려주기만 하면 되는 컴포넌트들을 먼저 구성해보자. local state 도, life cycle method 도 고려하지 말자.
물론, 그렇다고 해서 이런 Presentational Component 가 무조건 function component 여야 할 필요는 없다. 그렇지만 state, life cycle method 를 고려하지 않고 작성해도 되기 때문에 이왕이면 더욱 표현이 간단한 function component 로 작성하자는 이야기이다.
그러니 즉슨, 나중에 local state 나 life cycle method 나, 혹은 성능의 최적화를 고려할 때, 그 때는 class component 로 바꿔도 상관 없다.
components/Todo.js
import React from 'react'
import PropTypes from 'prop-types'
const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map((todo, index) => (
<Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} />
))}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
onTodoClick: PropTypes.func.isRequired
}
export default TodoList
components/Link.js
import React from 'react'
import PropTypes from 'prop-types'
const Link = ({ active, children, onClick }) => (
<button
onClick={onClick}
disabled={active}
style={{
marginLeft: '4px'
}}
>
{children}
</button>
)
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
components/Footer.js
import React from 'react'
import FilterLink from '../containers/FilterLink'
import { VisibilityFilters } from '../actions'
const Footer = () => {
<p>
Show: <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
{', '}
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
{', '}
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
</p>
}
export default Footer
The recommended way to start new apps with React Redux is by using the official Redux+JS template for Create React App, which takes advantage of Redux Toolkit.
참조한 글 : reduxjs/react-redux
React 를 개발할 때 Redux toolikit 을 백분 활용할 수 있게 해주는 방법이다. React 와 Redux 의 공식적인 최적화 된 조합을 사용하기 위해서 사용하는 방법이랄까?
참조한 글 :
Redux 문서에 나오는 Presentation Component
라는 개념이 이해가 가지 않아 해당 키워드로 구글링을 해 보니, 어떤 사람이 Presentational Component 와 Container Component 에 대해 작성한 글을 번역한 글이 나와 그 글을 참조하였다. 일단 생소한 개념이라 한 번 정독해보았다.
핵심을 적어보자면, state 가 필요한 것과 state 가 필요하지 않은 것에 대한 구분이겠다. 기술적이라기보다는 용도에 따른 차이이며, 밑에서도 "용도에 따른 차이" 라고 이야기한다.
Presentation Component, 말 그대로 "보여주는" 컴포넌트는 state 를 저장하는 stores(flux 나 redux 같은)에 의존적이지 않고 props 를 통해서 callback 함수와 데이터를 받으며 상태를 거의 가지고 있지 않다.
반면 Container Component, 말 그대로 "담고 있는" 컴포넌트는 state 를 저장하는 store 와 관련되어 action 을 호출하고 데이터(state)를 Presentation Component 에게 callback 형식으로 제공하는 역할을 한다.
저자는 이 두개를 명확히 구분하기 위해서 component 들을 구분한 뒤 다른 폴더에 생성한다고 한다. 이런 식으로 component 들을 용도에 따라 구분해서 얻을 수 있는 이점들은 다음과 같다고 한다
this.props.children
을 통해서 구현될 수 있다.확실히 납득이 간다. 만약 state 를 관리하는 코드들과 state 가 딱히 필요 없는 코드들을 구분해서 관리하게 된다면 유지보수를 할 때 그렇게 머리가 아플 일도 없을 것이다. 저자는 "언제 Container Component 를 도입해야 하는가?" 에 대해서도 조언을 해 주고 있다.
우선 앱을 만들때 Presentational Component 를 먼저 만드세요. 그러면 너무 많은 props 를 중간 Component로 보내야 한다는 것을 깨닫게 될것입니다. 전달받은 props 를 사용하지 않고 아래로 전달하기만 하는 Component나 자식 Component가 더 많은 데이터를 필요로 할때 모든 중간 Component를 재구성해야하는 Component들이 있다는 것을 알게 될것입니다. 바로 이 때 Container Component를 도입해야합니다. 데이터나 아무 상관없는 중간 컴포넌트에 대해 걱정이 없는 leaf Component의 행위가 담긴 props 를 얻을 수 있는 방법이 될 것입니다.
일단은 Presentational 위주로 애플리케이션을 구성하다가, props drilling 에서 "슬슬 골치가 아파지는데..."
싶을 때, "이 component 는 단지 props 를 내려주기 위해 중간에 존재하는 component 일 뿐인데, 그냥 state 를 도입해서 코드 구조를 조금 더 간결하게 할 순 없을까?" 할 때 만들어 보라고 한다.
Proptypes
in React참고 문서 : PropTypes와 함께 하는 타입 확인 - React
타입을 먼저 확인하는 React 가 기본제공하는 Type Check 라이브러리이다. 물론 TypeScript 를 사용하기도, Flow 를 사용해도 상관은 없다만, 이는 React 가 기본제공하는 라이브러리기 때문에 사용이 조금 더 용이할 수도 있다(실무에서는 어떻게 사용할 지 모르겠다만) 이렇게 타입을 체크하게 된다면, 내가 구현한 애플리케이션 스케일이 커졌을 때 오류를 잡기가 조금 더 쉬워진다.
//React 15.5 ver 부터 해당 라이브러리는 React.propTypes 가 아닌 prop-types 로 이동하였다
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
//component 의 property 의 type 을 이런 식으로 확인해 볼 수 있다
Greeting.propTypes = {
name: PropTypes.string,
//그리고 이렇게 requiredFunc 을 넣어서 적절한 타입의 prop 이 제공되지 않았을 때 경고를 띄울 수도 있다
requiredFunc: PropTypes.func.isRequired
};
참고로, 저 propTypes
부분을 requiredFunc
이 아니라 prop
하나하나마다 요구할 수도 있다. 그렇게 요구한다면 밑에서 requiredFunc
를 통해 한 번에 나타내 주지 않아도 된다.
Greeting.propTypes = {
//이렇게 PropTypes.specificType.isRequired 와 같이 표현해도 된다
name: PropTypes.string.isRequired
};