웹 개발에서 어느 컴포넌트에서 발생하는 값을 다른 컴포넌트로 이동시켜야하는 일은 흔하게 발생한다. 이 이동 작업을 이해하기 위해서는 2가지 개념을 알아야 한다.
부모 컴포넌트로부터 자식 컴포넌트로 전달되는, 일방적인 방향만으로 전달되는 변경 불가한 불변의 데이터.
위에서 아래로만 전달이 가능하며 한번 전달이 이루어지게 되면, 부모 컴포넌트에서 해당 값을 갱신하여 다시 내려주는 것 이외에는 값이 변화하지 않는다.
<부모 컴포넌트>
const Parent = () => {
const [data, setData] = useState(1);
return (
<Child props={data}>
);
};
<자식 컴포넌트>
const Child = (props) => {
return (
<div>
{props.data}
</div>
);
};
부모 컴포넌트에서 자식 컴포넌트로 필요한 데이터를 인자로 전달한다. 그리고 자식 컴포넌트는 전달된 'props'를 매개변수로 가져와 사용할 수 있다.
props는 기본적으로 상위 컴포넌트에서 지정한 명칭을 이름으로 갖는 객체 형태로 이루어져있다.
다만 자식 컴포넌트에서 구조분해 할당 문법을 사용하게 되면 해당 데이터를 바로 꺼내올 수 있다.
<자식 컴포넌트 2>
const Child = ({ data }) => {
return (
<div>
{data}
</div>
);
};
props를 이용하는 방법은 가장 간단하고, 직관적으로 사용할 수 있으며 다른 모듈이나 라이브러리를 설치할 필요가 없다. 다만 아래와 같은 단점들이 존재한다.
오직 부모 -> 자식 컴포넌트의 관계에서 '일방통행'으로만 데이터를 전달할 수 있다.
여러 개의 컴포넌트를 건너뛰어 데이터를 전달할 수 없다. 부모 컴포넌트 -> 자식 컴포넌트 -> 자손 컴포넌트 관계가 있다고 한다면, 부모에서 자손으로 바로 전달할 수 없고 반드시 자식 컴포넌트를 거쳐야한다.
프로젝트의 규모가 거대해질 수록 2번째 단점이 심각하게 다가오는데, 컴포넌트의 구조가 여럿 중첩될 경우 해당 데이터를 전혀 사용하지 않는 컴포넌트에도 props를 받아 아래로 전달해주어야 하기 때문에 코드의 가독성과 유지보수성이 점차 악화될 수 밖에 없다. 게다가 자식 컴포넌트가 리렌더링될 경우 부모 컴포넌트도 리렌더링 되는 React.js의 특성상 props 전달 구조가 복잡해지는 것은 곧 불필요한 리렌더링을 야기하여 성능의 저하까지 발생시키게 된다.
위에서 언급한, 해당 데이터를 전혀 사용하지 않는 컴포넌트에도 props를 받아 아래로 전달해주는 과정을 props drilling이라 부른다. 이를 해결하기 위한 방법은 2가지로, 하나는 전역 상태 관리 라이브러리를 사용하는 것. 나머지 하나는 React의 Children 패턴을 사용하는 것이다. Children 패턴에 대해서는 나중에 따로 포스트에서 다룰 예정이다.
상태는 객체이다. 하나의 객체 안에 App에서 필요한 모든 데이터를 저장하여 코드의 복잡성을 낮출 수 있다. 또한 어떤 동작을 할 때 이 하나의 객체의 내부 데이터를 수정하는 것 만으로 모든 작업을 구현할 수 있다.
일단 자신이 가진 데이터를 바꾸어야 하며, 다른 컴포넌트의 데이터를 모두 동시에 바꾸어주어야 한다. 컴포넌트의 숫자가 늘어나면 늘어날 수록 동일한 역할을 하는 함수가 덩달아 증가하면서 프로젝트의 효율성이 급락하게 된다.
코드가 불어나고, 유지보수의 난이도가 증폭되며 각 컴포넌트들이 의존 관계에 놓이면서 컴포넌트를 수정하거나 삭제하는 과정이 엄청나게 복잡해진다. 이를 커플링 관계에 놓여있다고 하는데, 이렇게 되면 부품(컴포넌트)라는 명칭조차 무색하게 컴포넌트인데 다른 곳에서 사용할 수가 없는 모습이 되어버리는 것이다.
각각의 컴포넌트들은 store를 중심으로 state를 변화시키고 받아오는 함수들로 연결된다. 각 컴포넌트들이 직접 연결되지 않기 때문에 코드가 쓸때없이 늘어나는 것을 제한할 수 있으며, 연결된 컴포넌트 하나가 사라져도 에러가 발생되지 않는다.
Redux를 통해 중앙 집중형 관리를 하면, 각각의 component는 action(상태가 바뀌었다는 것)을 store에 dispatch(통보)하게 된다.
이에 따른 자신의 변화를 작성 후 store에 subscribe(구독)하면, state가 바뀔 때마다 통보를 받기 때문에 다른 component와의 연결 없이 자신의 모양을 자유롭게 바꿀 수 있다.
즉, 수정해도 다른 부품들은 영향을 받지 않게됨으로써 Redux를 통해 각 Module의 독립성을 보장받을 수 있다.
서로 의존 관계에 놓여있지 않으며 이를 디커플링이라고 한다. 각각의 컴포넌트(부품)들이 스탠드 얼론 상태로 존재하게 되는 것이다!
Redux. 자바스크립트 앱을 위한 예측 가능한 상태 저장소. 전역 상태(state) 관리 라이브러리.
만약 프로젝트 내부에서 여러 컴포넌트에 동시에 사용되는 데이터를 한 곳에서 관리하며 다른 컴포넌트를 거치지 않고 직접 전달할 수 있으면 얼마나 편리할까?
props의 단점은 코드가 점차 복잡해진다는 것이고, 코드가 복잡해진다는 것은 프로그램 혹은 프로젝트의 유지보수 난이도는 기하급수적으로 치솟는다는 뜻이다. Redux는 이런 코드의 복잡성을 '낮추기' 위해서 개발되었다.
하나의 객체 안에 App에서 필요한 모든 데이터를 저장하여 코드의 복잡성을 낮출 수 있다.
2번 문단에서 state를 설명했을 때 위와 같은 특징을 언급한 바 있다. 그런데 이렇게 되면 언제 어디서든 모든 데이터에 접근이 가능하다는 뜻도 된다.
그렇기에 이 하나의 객체는 철저히 분리되어 관리되어야 한다. 권한을 가진 특정 대상만이 이 객체에 접근할 수 있도록 해야 한다는 것이다. Redux에서는 이를 위해 dispatcher, reducer에만 권한을 부여하였다. 반대로 객체에서 데이터를 받아오는 것은 오직 getstate만 가능하도록 하였다.
Redux의 핵심은, 데이터의 접근방식을 제한시켜두어 '예측 가능한' 상태 관리를 가능하게 하는 것.
Redux는 각각의 state를 철저하게 통제하고, state를 변경할 때 원본값을 바로 수정하지 않고 값을 복제한 다음 수정하여 불변성을 유지한다.
이 덕분에 UNDO와 REDO같이 App의 state를 바꾸는 작업을 간단하게 처리할 수 있다.
나아가 디버거 등을 통하여 App의 현재 상태를 확인할 때, Redux는 이전의 state도 모두 기록해두기 때문에 시간대를 돌려서 App의 state를 확인하여 문제 해결을 쉽게 만들어준다.
또, Redux의 이런 특성 덕분에 모듈 리로딩이 가능해진다. 코드를 작성하면 자동으로 App에 반영할 수 있는 것이다.
변경 사항의 반영을 위해서는 Refresh가 이루어져야한다. 그런데 이 과정에서 작성 한 data가 모두 초기화되어버린다. 핫 모듈 리로딩은 data의 초기화를 막아준다. Refresh가 이루어져도 date는 그대로 유지할 수 있는 것.
Redux는 App의 state를 이전보다 더 예측 가능하도록 개선할 수 있고, 견고하면서도 유동적인 App의 개발이 가능할 수 있게 만든다.
(...) React Component -> Action -> Reducer -> Store -> React Component (...)
Action,
state 변경을 요청하는 객체. state가 변화해야하는 상황이 올 때 Action은 관련 내용들을 받아 Reducer로 전달한다.
{ type: '', ... }의 형태로 이루어져 상황을 특정하는 type 정보와 필요한 데이터들을 담고있다.
수행하는 작업의 유형은 type, 상황에 따라서 payload 속성으로 데이터를 받을 수도 있다.
Reducer,
이전 State와 action 객체를 받아서 next state를 return해주는 역할. state의 변경 상태를 관리하고 상태 업데이트와 최종값을 반환하는 역할을 맡은 함수.
Store,
해당 App의 모든 state 트리를 보유하는 존재. state를 관리하는 함수들을 가지고 있다.
프론트에서 어떤 상태 변화 이벤트가 발생하면, Event Handler와 Dispatch를 통해 Store로 이동된다. 여기서 Reducer를 통해 상태가 변화하고 최종적으로 다시 프론트로 향한다.
render,
store 밖에 존재하는 개념, UI를 만들어주는 역할을 수행한다.
subscribe,
store의 state가 바뀔 때마다 자동으로 화면을 리렌더링 시켜주는 역할을 수행한다. state 값이 변경 될 때마다 render 함수가 실행되면서 화면을 갱신한다.
Redux와 함께 사용되는 개발 도구. App의 상태를 효과적으로 관리하기 위한 라이브러리로서, App의 상태 변화를 예측 가능하고 추적 가능한 방식으로 관리할 수 있도록 도와준다. 웹 브라우저에 설치하여 사용한다.
Redux의 Devtool은 다음과 같은 장점을 제공해준다.
이전 상태와 다음 상태 간의 변화를 시각적으로 확인할 수 있다.
디스패치된 액션들을 로깅하고, 각 액션에 대한 정보를 제공하여 액션들이 어떻게 상태 변화에 영향을 주는지 이해할 수 있다.
현재 Redux 상태의 내용을 확인할 수 있다. State 트리 혹은 특정 State의 세부 정보를 확인할 수 있다.
이전 상태와 현재 상태의 차이점을 시각적으로 비교하여 어떤 부분이 변경되었는지 파악할 수 있다.
특정 액션을 수동으로 디스패치하여 상태 변화를 시뮬레이션하고 테스트할 수 있다.
그런데 사실 React.js는 자체적으로 전역 상태 관리 라이브러리를 제공해주고 있다. 바로 Context API. 그런데 다음과 같은 이유로 Redux를 완전히 대체할 수는 없다.
Context가 업데이트될 때 모든 하위 컴포넌트가 리렌더링되면서 성능 저하가 일어날 수 있다.
DevTools을 제공하지 않아서 상태 모니터링과 디버깅 등의 작업을 수행하는데 어려움이 있다.
미들웨어를 지원하지 않아 비동기 작업, 로깅, 라우팅 등의 다양한 작업들을 처리할 수 없다.
다만 Redux에 비해서 아래와 같은 장점들이 있다. 상황에 따라서 Context API와 Redux를 선택하여 사용하는 것이 좋다.
Context API는 React의 일부로 제공되기 때문에 별도의 라이브러리를 추가로 설치할 필요가 없다.
Redux에 비해 사용 방법을 익히는 것이 쉽고 여러 설정들을 구현할 필요가 없어 사용이 매우 간편하다.
많은 것을 배웠습니다, 감사합니다.