- 0
- Seia
- 조회 수 311
제가 솔직히 말하자면 프로그래밍에 있어서 전문가도 아니지만 기본적으로 많은 분들이 React와 Redux에 대해서 어려움을 느끼고 가까이 쉽게 다가가지 못한다는 것을 느끼고 있어 글을 남기게 되었습니다. 나중에 제가 다시 보더라도 다시 쉽게 이해할 수 있었으면 좋겠네요.
기본적으로 React와 Redux는 웹 프로그래밍 라이브러리/프레임워크(는 아니지만 프레임워크만큼 strict하게 사용되는 경우가 많습니다) 중 가장 함수형 프로그래밍에 가까운 라이브러리입니다. 덕분에 저에게는 더욱 깔끔하게 느껴지지만 적용과 손에 익기까지 꽤 오랜 시간이 걸립니다.
비록 Redux가 함수형 프로그래밍의 위치에 있다고는 하지만 일단은 순수함수니 어쩌니 하는 것들은 제외하고 가겠습니다. 그 이유는 프로그래밍 방법론들이 언제나 많은 곳에서 활용되는 것은 사실이나 여러분이 무언가 프로그래밍을 할 때에는 분명히 순서라는 것이 있기 때문입니다. 적절히 전략들을 섞어써야 하는 것이지 하나의 방법론에만 몰두하게 된다면 미처 생각하지 못한 문제점도 방법론에 대한 과신뢰로 해결하지 못할 수 있습니다. 그리고 쉽게 이해하고 더 가까이 다가가게 하려면 어려운 요소보다는 조금 더 사용에 관하여 이야기하는 것이 어떨까 했습니다. (물론 세상에 교육적인 것으로 배워서 나쁠 것은 없다고 생각합니다.)
이미 수학식을 써버렸습니다.
그리고 여기에서는 React의 함수형 컴포넌트에서의 사용만 다룹니다.
먼저 React의 컴포넌트 사이에서는 서로의 상태(state)를 공유하지 못합니다. 함수형 프로그래밍의 형태를 어느정도 띠고 있기 때문에 하나의 컴포넌트에서 시작된 일은 하나의 컴포넌트에서 끝나야 합니다. 혹은 Express나 Koa 등을 살펴보세요. 100%는 맞는 이야기는 아닐지라도 Node.JS의 많은 웹 프레임워크들은 '미들웨어'라는 시스템을 가지고 있고 이 '미들웨어'는 수직적이며 연속적입니다. 이처럼 React의 컴포넌트들도 React DOM의 render 함수 아래에서는 연속적이며 수직적인 구조를 가지고 있습니다. (특정 컴포넌트에 refer는 가능하지만 이는 React가 V-DOM 라이브러리이기 때문에 성능 등의 문제로 Best pratice가 될 수는 없을 것입니다) 또한 하나의 컴포넌트는 render 함수 혹은 함수형 컴포넌트에서는 그 자체가 render 함수의 역할을 하기 때문에 (re)hydration을 거치는 순간 상태를 유지하기가 더 애매해집니다.
그러나 최근 React에서 함수형 컴포넌트 지원을 대폭 확대하기 위함을 하나의 이유로 삼고 나온 Hooks를 살펴보고 useInput, useSomething, use... 등의 여러가지 hook을 만들고 있다보면 생각이 들 것입니다. Hook을 사용하여 외부에 간접적으로 state store를 두는 것이 가능하지 않을까? 하는 생각입니다. 실제로 Redux를 사용하지 않더라도 React hooks만을 사용하여 외부에 store를 두도록 할 수 있습니다.
이러한 생각을 가지고 우리는 첫 번째로 Redux를 생각해봅니다.
Redux 또한 일종의 확실한 라이프타임을 가지고 있습니다. 하지만 때때로 '왜 이것이 필요하지?'라는 의문점이 들 수도 있습니다.
먼저 Redux의 라이프타임을 살펴보겠습니다.
(*출처: https://www.slideshare.net/visualengin/workshop-22-reactredux-m)
Redux는 Action과 Reducer로 상태를 관리합니다.
먼저 Action은 일종의 Serialization/정규화를 담당하는 함수로 Reducer에게 일정한 형태의 작업물을 가져다주는 역할을 하고 여러분의 코드 베이스를 더욱이 깔끔하게 하는 역할도 합니다. 저는 개인적으로 처음에 Action이라는 개념이 굳이 필수적인 개념인가에 대하여 오랜 시간 생각을 한 결과로 경험 상 없다면 만들어야 하는 함수 집합이라고 생각이 되었습니다.
예시 코드(Action creator, 액션 생성자 = action 객체를 생성함)를 보면...
// <CODE START> action/index.js
export const SAVE_USERENAME = 'SAVE_USERNAME'
export const saveUsername = username => {
return {
type: SASVE_USERNAME,
payload: username
}
}
// <CODE END>
우리는 모든 종류의 정규화 과정을 이 saveUsername이라는 함수(Action creator)에 넘겨줌으로써 기존의 코드에서 정규화할 수 있는 코드를 분리해낼 수 있습니다. 아래와 같이 수정을 할 수도 있겠죠.
// <CODE START> action/index.js
export const SAVE_USERENAME = 'SAVE_USERNAME'
export const saveUsername = username => {
username = username || 'user' + Math.floor(Math.random() * 1000)
return {
type: SASVE_USERNAME,
payload: username
}
}
// <CODE END>
이 코드에서 저희가 <<<<< username = username || 'user' + Math.floor(Math.random() * 1000) >>>>> username이라는 변수를 컴포넌트 측에서 처리하지 않고 action에서 처리하게 할 수 있도록 의도할 수 있습니다. 더욱 깔끔한 코드와 쉬운 유지보수를 위해서 말이죠. 또한 이러한 정규화된 코드는 저희가 문서화할 수 있도록 많은 도움을 줍니다. 꼭 값의 정규화를 Action 생성자에서 하지 않더라도 말이죠. 이러한 정규화는 문서화 뿐만 아니라 확장성 그리고 유닛 테스트 등에서도 빛을 발하는 중요한 요소입니다. 앞으로 애플리케이션을 만들어가는데에 있어서 좋은 확장성을 가질 수 있도록 해주는 하나의 습관이라고 생각합니다.
그리고 이 Action creator가 만든 객체 <<<<< { type: ..., payload } >>>>>가 Action이라고 불리고 Reducer가 이를 처리하게 됩니다.
Reducer는 Action을 통해 State를 수정합니다.
// <CODE START> reducers/index.js
import * as actions from '../actions'
const initState = {
username: 'user'
}
export default (state = initState, action) => {
switch (action.type) {
case actions.SAVE_USERNAME:
return {
...state, // NOTE: 실제로 현재 reducer에서는 username이라는 하나의 값만 다루니 필요가 없는 라인입니다.
username: action.payload
}
default:
return state
}
}
// <CODE END>
이제 Reducer는 Redux에서 State(상태)를 직접적으로 관리하는 함수입니다. 다른 함수에서는 애플리케이션의 상태를 직접적으로 변경해서는 안 됩니다. 이는 프로그램의 불확실성을 줄여주는 역할을 하며 동시에 간섭을 최소화하기도 합니다. 우리는 단순히 이를 <<<<< initial state + action = state >>>>>와 같은 식으로 표현할 수 있습니다. 마치 도형의 평형이동과 같은 느낌입니다. 하나의 값이 하나의 차원이라고 하였을 때 우리는 state(상태)를 하나의 도형이라고 할 수 있습니다.
[(중요!) 왜 <<<<< { ...state } >>>>로 새로운 객체를 반환하나요?]
Redux에서 상태는 불변성(변할 수 없음)을 가집니다. 그렇기 때문에 우리는 항상 새로운 상태를 반환하는 Reducer를 작성하게 됩니다. 하지만 언제나 그렇듯이 인간은 실수를 반복합니다. Switch 구문의 default case의 경우에는 <<<<< return { ...state } >>>>>가 아닌 <<<<< return state >>>>>로 반환합니다. 같은 객체가 반환되는 것 같아보여도 2번째 구문은 원래의 상태를 그대로 반환하지 불변성을 잃을 수 있습니다. 하지만 default case가 사용되는 경우는 2가지 뿐이기 때문인데 그 2가지 모두 <<<<< return state >>>>>를 사용하여도 새 객체를 반환하게 됩니다. 먼저 initState가 대신 사용되는 경우입니다. 처음 객체를 사용할 때에 또는 의도적으로 '처음에' 상태가 요구되었을 때 <<<<< state = initState >>>>> 구문에 의해서 state 변수는 initState가 됩니다. 이렇게 initState는 소비되었고 더 이상 변하면 안 되는 객체가 되었습니다. 그리고 두 번째 경우는 initState 객체가 사용되고 잘못된 Action Type이 주어지는 경우입니다. 이 경우에는 단순히 관계가 없는 Action이 입력되었으니 무시하고 원래의 상태를 그대로 반환합니다. 변경 사항이 없으니 OK입니다.
음... 나중에도 제가 이 글을 다시 보며 오류를 해결하길 바라며 아카이브합니다.