S3. [React] 상태 관리

Haizel·2023년 2월 13일
0

Front-End Developer 되기

목록 보기
54/70
post-thumbnail

01. 전역 상태 관리


상태란 ? → “UI에 동적으로 표현되는 데이터”를 말한다.

상태는 2가지로 나눌 수 있다.

  • 상태 변경이 일어나는 곳
  • 상태 변경의 영향을 받는 곳

🔨 1. 상태의 주요 고려대상, Side effect

💡 Side Effect란? : 함수(또는 컴포넌트)의 입력 외에도 함수의 결과에 영향을 미치는 요인을 말한다. ex) 네트워크 요청(백엔드 API 요청)
  • fetch와 같은 API 요청이 필요한 컴포넌트를 만들 때 → Side Effect가 불가피하게 발생하고, 또 side effect에 의존적인 상태도 있을 수 있다.
side effect에 의존적인 상태
'장바구니 목록 데이터' 가 서버에 있다면 fetch와 같은 네트워크 요청이 필요하고
이때 데이터 로딩 여부(isLoading)상태가 true/false 인지에 따라 "로딩 중"의 화면을 보여주게 되면 
=> 이것이 바로 side effect에 의존적인 상태이다.

🔨 2. 상태의 적절한 위치는 ? : 상태의 두가지 구분

1. 로컬상태

  • 특정 컴포넌트 안에서만 관리되는 상태
  • 다른 컴포넌트와 데이터를 공유하지 않는 폼(form) 데이터는 대부분 로컬 상태로, input box, select box등과 같이 입력값을 받는 경우가 해당된다

2. 전역상태

  • 프로덕트 전체 혹은 여러 컴포넌트에서 관리되는 상태로 다른 컴포넌트와 상태를 공유하고 영향을 끼친다.
  • 데이터 로딩 여부(로딩 중) 상태가 대표적 예로, 앱 전반에 영향을 준다.

3. 데이터 무결성(상태와 출처의 관계)

  • 서로 다른 컴포넌트가 사용하는 상태의 종류가 다르면 → 전역 상태일 필요도 없고, 출처(source)도 달라도 된다.
  • 하지만 서로 다른 컴포넌트가 동일한 상태를 다룬다면 → 출처는 오직 한 곳, 하나의 출처(전역 공간)를 가져야 한다.

⇒ Single Source Of Truth(신뢰할 수 있는 단일 출처) 원칙

  • 데이터 무결성을 위해 → 동일한 데이터는 항상 같은 곳에서 데이터를 가져와야 한다.

🔨 3. 전역 상태 관리의 예

  • 테마 설정
    • 라이트 모드/다크 모드의 테마 설정
    • 모든 페이지, 컴포넌트에 테마가 적용되어야 하므로 → 전역관리
  • 국제화 설정
    • 사용자가 설정한 언어설정(Korea, Elglish 등)을 → 전역관리해, 모든 브라우저, 운영체제에 적용

🔨 4. 상태관리 툴과 특징

💡 **대표 상태 관리 라이브러리**
  • React Context
  • Redux
  • Mobx

상태 관리 라이브러리 장점

  1. 전역 상태를 위한 저장소를 제공한다.
  2. props drilling(프로퍼티 내려꽂기) 문제를 해결한다.
  3. 상태의 출처 파악이 쉬워져 → 코드의 유지보수가 좋다.
But, 상태 관리 툴은 반드시 필요하진 않다. 대부분의 경우 "React 사고하기" 를 통해 해결 가능하다.

02. Props Drilling


🔨 1. Props Driling이란?

  • 상위 컴포넌트의 State를 컴포넌트 D에게 props로 전달하고자, 중간에 위치한 컴포넌트 B, C에 props을 만들어 컴포넌트 D에게 props데이터를 전달하는 현상을 말하는데, 프로퍼티 내리꽂기라고도 한다.
  • 즉 A의 state를 컴포넌트 D로 전달하기 위해 컴포넌트 B,C를 거쳐야 한다.

🔨 2. Props Driling의 문제점

  1. 코드의 가독성이 매우 나빠진다.
  2. 코드의 유지보수도 어렵다.
  3. state 변경 시 Props 전달을 위해 불필요하게 관여된 컴포넌트도 리렌더링되어 → 웹 성능에 악영향을 끼칠 수 있다.

🔨 3. 해결방법

  • (1)컴포넌트와 관련이 있는 state를 가까이 보관하거나,
  • (2)상태 관리 라이브러리를 사용해 → 전역으로 관리하는 저장소에 직접 state를 꺼내쓸 수 있다.
  • 이를 통해 props Driling을 방지할 수 있다.
  • 대표적 상태 관리 라이브러리(Redux, Context api, Mobx, Recoil 등) 중 Redux에 대해 배워보자.

03. **Props Drilling 예시**


🔨 예시 1.

Q. 7시 방향에 있는 `Edit on StackBlitz`를 눌러서 StackBlitz에 접속하세요.
  • Props Drilling
import React, { useState } from 'react';
import styled from 'styled-components';

const Container = styled.div`
  border: 5px solid green;
  padding: 10px;
  margin: 10px;
  position: relative;
`;

const Quantity = styled.h2`
  text-align: center;
  color: red;
  border: 5px solid red;
  padding: 3px;
`;

const Button = styled.button`
  margin-right: 5px;
`;

const Text = styled.h1`
  color: ${(props) => (props.color ? props.color : 'black')}
`;

export default function App() {
  const [number, setNumber] = useState(1);

  const plusNum = () => {
    setNumber(number + 1);
  };

  const minusNum = () => {
    setNumber(number - 1);
  };
  console.log('Parents');
  return (
    <Container>
      <Text>[Parents Component]</Text>
      <Text>
        Child4 컴포넌트에 있는 버튼을 통해
        <br /> state를 변경하려고 합니다.. 🤮
      </Text>
      <Text color="tomato">Props Driling이 발생!!</Text>
      <Quantity>{`수량 : ${number}`}</Quantity>
      <Child1 plusNum={plusNum} minusNum={minusNum} />
    </Container>
  );
}

function Child1(
  {
    /* props로 전달받은 plusNum, minusNum를 가져오세요 */
  }
) {
  console.log('Child1');
  return (
    <Container>
      <Text>[Child 1 Component]</Text>
      {/* plusNum, minusNum 함수를 props로 전달해주세요! */}
      <Child2 />
    </Container>
  );
}

function Child2(
  {
    /* props로 전달받은 plusNum, minusNum를 가져오세요 */
  }
) {
  console.log('Child2');
  return (
    <Container>
      <Text>[Child 2 Component]</Text>
      {/* plusNum, minusNum 함수를 props로 전달해주세요! */}
      <Child3 />
    </Container>
  );
}

function Child3(
  {
    /* props로 전달받은 plusNum, minusNum를 가져오세요 */
  }
) {
  console.log('Child3');
  return (
    <Container>
      <Text>[Child 3 Component]</Text>
      {/* plusNum, minusNum 함수를 props로 전달해주세요! */}
      <Child4 />
    </Container>
  );
}

function Child4({ plusNum, minusNum }) {
  console.log('Child4');
  return (
    <Container>
      <Text>[Child 4 Component]</Text>
      <Button onClick={plusNum}>👍</Button>
      <Button onClick={minusNum}>👎</Button>
    </Container>
  );
}
  • 상태관리 라이브러리 적용
import React, { useState } from 'react';
import styled from 'styled-components';
import { useSelector, useDispatch } from 'react-redux';

const Container = styled.div`
  border: 5px solid green;
  padding: 10px;
  margin: 10px;
`;

const Quantity = styled.h2`
  text-align: center;
  color: red;
  border: 5px solid red;
  padding: 3px;
`;

const Button = styled.button`
  margin-right: 5px;
`;

const Text = styled.h1`
  color: ${(props) => (props.color ? props.color : 'black')}
`;

export default function App() {
  const number = useSelector((state) => state);
  console.log('Parents');
  return (
    <Container>
      <Text>[Parents Component]</Text>
      <Text>
        Child4 컴포넌트에 있는 버튼을 통해 <br /> state를 변경하려고 합니다. ☺️
      </Text>
      <Text color="tomato">(Redux를 사용하는 경우)</Text>
      <Quantity>{`수량 : ${number}`}</Quantity>
      <Child1 />
    </Container>
  );
}

function Child1() {
  console.log('Child1');
  return (
    <Container>
      <Text>[Child 1 Component]</Text>
      <Child2 />
    </Container>
  );
}

function Child2() {
  console.log('Child2');
  return (
    <Container>
      <Text>[Child 2 Component]</Text>
      <Child3 />
    </Container>
  );
}

function Child3() {
  console.log('Child3');
  return (
    <Container>
      <Text>[Child 3 Component]</Text>
      <Child4 />
    </Container>
  );
}

function Child4() {
  const dispatch = useDispatch();
  const plusNum = () => {
    dispatch({ type: 'Plus' });
  };

  const minusNum = () => {
    dispatch({ type: 'Minus' });
  };
  console.log('Child4');
  return (
    <Container>
      <Text>[Child 4 Component]</Text>
      <Button onClick={plusNum}>👍</Button>
      <Button onClick={minusNum}>👎</Button>
    </Container>
  );
}

🔨 예시2.

Q. Child6 에 있는 👋 버튼을 누르면 Child3에 느낌표가 하나씩 추가되는 간단한 애플리케이션을 만들자.
- 이 때, Child3, Child6이 하나의 상태를 공유하기 때문에 최상위 컴포넌트인 App에서 상태를 관리해야 한다.
-> 때문에 상태를 변경할 때마다 App 컴포넌트가 리렌더링 되면서 모든 컴포넌트가 리렌더링된다. 
=> 변경되는 상태와 연관 없는 컴포넌트까지 불필요하게 리렌더링되는 현상을 해결하자.
  • Props Drilling으로 인한 불필요한 리렌더링
import * as React from 'react';
import './style.css';
import styled from 'styled-components';
import { useState } from 'react';

const Component = styled.div`
  border: 3px solid green;
  border-radius: 10px;
  flex-grow: 1;
  line-height: 30px;
  text-align: center;
  margin: 10px;
  >button{
    margin-left: 10px;
  }
`;

const Container = styled.div`
  display: flex;
  width: 100%;
  justify-contents: center;
`;

export default function App() {
  const [greeting, setGreeting] = useState('Hello');

  console.log('App');
  return (
    <Container>
      <Component>
        App
        <Container>
          <Child1 greeting={greeting} setGreeting={setGreeting} />
          <Child2 greeting={greeting} setGreeting={setGreeting} />
        </Container>
      </Component>
    </Container>
  );
}

function Child1({ greeting, setGreeting }) {
  console.log('Child1');
  return (
    <Component>
      Child1
      <Container>
        <Child3 greeting={greeting} setGreeting={setGreeting} />
        <Child4 />
      </Container>
    </Component>
  );
}

function Child2({ greeting, setGreeting }) {
  console.log('Child2');
  return (
    <Component>
      Child2
      <Container>
        <Child5 />
        <Child6 greeting={greeting} setGreeting={setGreeting} />
      </Container>
    </Component>
  );
}

function Child3({ greeting, setGreeting }) {
  console.log('Child3');
  return <Component>Child3 : {greeting} </Component>;
}

function Child4() {
  console.log('Child4');
  return <Component>Child4</Component>;
}

function Child5() {
  console.log('Child5');
  return <Component>Child5</Component>;
}

function Child6({ greeting, setGreeting }) {
  console.log('Child6');
  return (
    <Component>
      Child6
      <button onClick={() => setGreeting(greeting + '!')}>👋</button>
    </Component>
  );
}
  • Redux의 리렌더링 최적화
import * as React from 'react';
import './style.css';
import styled from 'styled-components';
import { useSelector, useDispatch } from 'react-redux';

const Component = styled.div`
  border: 3px solid green;
  border-radius: 10px;
  flex-grow: 1;
  line-height: 30px;
  text-align: center;
  margin: 10px;
  >button{
    margin-left: 10px;
  }
`;

const Container = styled.div`
  display: flex;
  width: 100%;
  justify-contents: center;
`;

export default function App() {
  console.log('App');
  return (
    <Container>
      <Component>
        App
        <Container>
          <Child1 />
          <Child2 />
        </Container>
      </Component>
    </Container>
  );
}

function Child1() {
  console.log('Child1');
  return (
    <Component>
      Child1
      <Container>
        <Child3 />
        <Child4 />
      </Container>
    </Component>
  );
}

function Child2() {
  console.log('Child2');
  return (
    <Component>
      Child2
      <Container>
        <Child5 />
        <Child6 />
      </Container>
    </Component>
  );
}

function Child3() {
  const greeting = useSelector((state) => state);
  console.log('Child3');
  return <Component>Child3 : {greeting} </Component>;
}

function Child4() {
  console.log('Child4');
  return <Component>Child4</Component>;
}

function Child5() {
  console.log('Child5');
  return <Component>Child5</Component>;
}

function Child6() {
  console.log('Child6');
  const dispatch = useDispatch();
  const addBang = () => {
    dispatch({ type: 'AddBang' });
  };
  return (
    <Component>
      Child6
      <button onClick={addBang}>👋</button>
    </Component>
  );
}
profile
한입 크기로 베어먹는 개발지식 🍰

0개의 댓글