[리액트 개발 시, state 관리의 문제점]
상태(state)가 업데이트되면 컴포넌트가 렌더링된다.
그리고, 이 컴포넌트의 하위 컴포넌트들은 모두 자동으로 리-렌더링된다.
리-랜더링이 된다는 말은 로딩 시간이 더욱 길어진다는 뜻이다. 불필요한 렌더링은 막아야 한다.
소규모 프로젝트에서는 이러한 불필요한 렌더링을 shouldComponentUpdate를 구현하여 방지할 수 있다.
이 shouldComponentUpdate 함수를 사용하면 render()함수를 실행시키지 않기때문에 렌더링을 하지 않는다.
하지만, 프로젝트가 대규모가 된다면 트리 구조 상 너무 복잡해지며, 일일히 잡아주기도 한계가 있다.
또한, 하나의 상위 컴포넌트에서 모든 상태(state)를 관리한다면,
2단계 컴포넌트 자신은 필요가 없는데, 3단계 자식 컴포넌트 때문에 갖고 있어야 한다.
따라서, 불필요한 props의 개수가 많아지게 된다.
여러 컴포넌트를 거처서 props를 전달하는 것은 비효율적이며, 가독성이 떨어지게 된다.
이러한 다양한 문제점을 방지하기 위해 Redux를 사용한다.
리액트에서 컴포넌트 트리를 정리해보면 다음과 같다.
리액트는 이러한 계층 구조로 이루어진다. 또한, 루트 컴포넌트(App.js)에서 기본적으로 모든 상태를 관리한다.
그래서, App.js 컴포넌트의 상태를 업데이트하면 App 컴포넌트뿐만 아니라, 모든 하위 컴포넌트들도 리렌더링된다.
리액트 프로젝트 환경에서는 부모 컴포넌트(App.js)를 거처야 서로 소통이 가능하다.
부모 컴포넌트를 거치지 않고 직접 소통할 수는 있지만 코드가 꼬여버리기 때문에(스파게티 소스)
절대 그렇게 해서는 안 된다. 그러다보니, 위에서 말한 것과 같이 불필요한 props가 발생되는 것이다.
여러 컴포넌트를 거쳐서 props를 전달하는 것은 비효율적이다.
- 작업할 때, 가독성이 떨어지며,
- props의 개수가 너무 많아질 수도 있다.
만약, G에서 필요한 값을 위해서 App.js B와 C에 쓰이지도 않는 props를 G를 위해 선언해야 한다.
App.js에서는, <B name={"SUNGHO"} />
B에서는 , <C name={this.props.name} />
C에서는, <G name={this.props.name} />
이렇게 보내줘야 G에서 받아서 쓰는 것이다.
그런데, 만약 name 속성을 바꾼다면? 또 3개의 파일을 각각 열어서 name을 일일히 바꿔줘야 한다.
복잡해질수록, 유지보수 역시 최악으로 간다.
[리덕스 개념]
리덕스가 없는 리액트는 상태(state)를 컴포넌트 자신이 자체적으로 상태 관리한다.
반면, 리덕스를 사용한 리액트는 상태 관리의 로직을 컴포넌트 외부에서 관리하는 것이다.
즉, 상태를 좀 더 효율적으로 관리하는데 사용하는 상태 관리 라이브러리라고 할 수 있다.
리덕스를 사용하면 스토어(Store)라는 객체 내부에 상태에 대한 데이터를 담아 관리할 수 있게 된다.
스토어는 리액트 개발 프로젝트상의 상태에 대한 데이터값들을 내장하고 있다.
정리
리덕스를 사용하게되면,
1. 스토어에서 모든 상태 관리를 한다.
2. 상태에 어떤 변화를 일으켜야할 때는 액션을 스토어에 전달한다.
액션 = 객체 형태이고, 상태를 변화시킬 때 이 객체를 참조해서 변화를 일으킨다
3. 액션을 전달하는 과정을 디스패치(Dispatch)라 한다.
4. 스토어가 액션을 받으면 리듀서가 전달받은 액션을 기반으로 상태를 어떻게 변화시킬지 정한다.
액션을 처리하면 새 상태를 스토어에 저장한다.
5. 스토어의 값이 바뀌면 스토어를 구독(subscribe)하고 있는 컴포넌트에 바로 전달한다.
이렇게 부모 컴포넌트로 props를 전달하는 작업은 생략하며,
리덕스에 연결하는 함수를 사용하여 컴포넌트를 스토어에 구독시킨다.
스토어가 생기면,
G는 더이상 App.js -> B -> C 순으로 props를 받아서 쓰지 않고, 다이렉트로 스토어에 값을 달라고 요청을 한다.
이것을 "G 컴포넌트가 스토어에 구독(Subscribe)한다"고 말할 수 있다.
구독을 하게되면, 나중에 스토어 안에 있는 상태 변동이 있다면, 구독하고 있는 컴포넌트에 바로 전달한다.
만약, B에서 뭔가 이벤트가 발생해서 상태를 변경(Update)하고자할 때는,
dispatch라는 함수를 통해서 액션을 스토어에게 전달한다.
액션(Action)은 객체 형태로 되어 있으며, 상태를 변화시킬때 이 객체를 참조하여 변화를 일으킨다.
이 액션을 전달하는 과정을 디스패치(Dispatch)라 부른다.
스토어가 액션을 받으면 리듀서가 전달받은 액션을 기반으로 상태를 어떻게 변경해야 할 지 정한다.
액션을 처리하게 되면 새 상태를 스토어에 저장한다.
정리
1. 스토어 : 리액트 프로젝트 내 상태 값들을 내장하고 있는 객체.
2. 액션 : 상태 변화를 일으킬 때 참조하는 객체.
3. 디스패치 : 액션을 스토어에 전달하는 것을 의미함.
4. 리듀서 : 상태를 변화시키는 로직이 있는 함수.
5. 구독 : 스토어 값이 필요한 컴포넌트들은 스토어를 구독.
리덕스는 리액트에 종속적(의존적)이지 않다.
즉, 리액트를 사용하지 않아도 리덕스를 사용할 수 있다.
<액션 & 액션 생성 함수>
[액션]
액션은 스토어에서 상태 변화를 일으킬 때 참조하는 객체이며, 이 객체는 반드시 type값이 있어야 한다.
type만 고정이며, 다른 것들은 유동적이므로 자유롭게 선택해 사용하면 된다.
액션 타입은 해당 액션이 어떤 작업을 하는 액션인지 정의한다.(대문자와 밑줄(_) 조합으로 생성)
하지만, 액션을 새로 만들때마다 직접 객체를 만든다면 액션 형식을 모두 알아야하므로 불편하다.
따라서, 액션을 만들어주는 함수를 사용하는데, 이를 액션 생성 함수라 부른다.
[액션 생성 함수]
1. 먼저 액션 타입을 상수값으로 정의한다.
const INCREMENT;
const POINT_NUMBER;
2. 액션 생성 함수를 생성한다.
const increment = () => ({
type: INCREMENT
})
const point = () => ({
type: POINT_NUMBER
})
type외에 다른 값이 들어가야 한다면, 파라미터에 넣으면 된다.
const point = (name) => ({
type: POINT_NUMBER
})
const INCREMENT = 'INCREMENT';
const POINT_NUMBER = 'POINT_NUMBER';
const increment = () => ({
type: INCREMENT
})
const point = (point) => ({
type: POINT_NUMBER,
point: point
})
console.log(increment());
console.log(point(19));
위에서 생성한 액션 생성 함수의 결과는 다음과 같다.
<리듀서>
- 상태에 변화를 일으키는 함수이며, 파라미터를 두 개 받는다.
1. 첫 번째는, 현재 상태
2. 두 번째는, 액션 객체
함수 내부에서는 switch문을 활용해서 action.type에 따라 새로운 상태를 만들어서 반환해야 한다.
리듀서가 초기에 사용할 초기 상태값부터 먼저 설정해야 리듀서를 만들 수 있다.
리덕스 스토어를 구독한다는 것은 리덕스 스토어의 상태가 변경될 때마다 특정 함수를 실행시킨다는 의미.
리덕스를 사용할 때 주의해야 할 세 가지
1. 스토어는 단 한 개
- 스토어는 언제나 한 개
- 스토어 여러개 생성 후 상태를 관리해서는 안 된다.
- 대신, 리듀서를 여러개 만들어서 관리할 수 있다.
2. state는 읽기 전용
- 리덕스의 상태(state)는 읽기 전용이다. 그러므로, 이 값을 절대로 수정해서는 안 된다.
- 수정을 하게 되면 리덕스의 구독 함수를 제대로 실행하지 않거나 컴포넌트의 리렌더링이 안 될 수 있다.
- 상태를 업데이트할때는 새 상태 객체를 만들어서 넣어주어야 한다.
3. 변화는 순수 함수로 구성
- 모든 변화는 순수 함수로 구성해야 함. 함수란 리듀서 함수를 뜻한다.
- 순수 함수에서 결과값을 출력할때는 파라미터 값에만 의존해야 하