React로 사고하기

Yeom Jae Seon·2021년 2월 12일
0

React공식문서 공부

목록 보기
11/11
post-thumbnail

그럼 지금까지 배운 내용을 토대로 React로 앱을 설계해보자.

React의 장점중 하나가 앱을 설계하는 방식이다.
지금까지 배운 내용을 토대로 앱을 설계해보자.

만들 앱


[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

이 JSON데이터를 외부로 부터 받아서 사용자에게 보여주는 앱을 설계해보자.
이 앱은 사용자가 검색할수도 있다.

1단계: UI를 컴포넌트 계층 구조로 나누기


일단 박스모델을 만드는것이 첫번째이다.
모든 컴포넌트(혹은 그 하위 컴포넌트)의 주변에 박스를 그리고 각각에 이름을 붙이자.

여기선 눈에 보이는 UI를 기반으로 죄다 박스모델을 만드는 것이 중요하다.
(컴포넌트로 생각하기 이전의 단계이다.)

죄다 박스모델로 만드는 동시에 같은 기능을 수행하는 박스들은 색깔을 같게했다.
여기서 같은 색깔로 묶인 박스들은 하나의 컴포넌트로써 존재한다.

컴포넌트도 함수와 같이 하나의 기능만 수행해야하며 여러개의 기능을 수행하는 것 같다고 느끼면 하위 여러 컴포넌트로 분리하야여한다.
이렇게 컴포넌트를 구성할때는 단일 책임 원칙을 생각하며 컴포넌트를 구성한다.

1. 빨간색 박스모델 : 앱 전체를 포괄한다. : Container
2. 주황색 박스모델 : 사용자가 입력할수 있는 부분. : UserInput
3. 노란색 박스모델 : 데이터 목록 전체를 보여주는 부분. : DataContainer
4. 초록색 박스모델 : 데이터 목록 종류를 보여주는 부분. : DataHeader
5. 파란색 박스모델 : 데이터 하나 하나를 보여주는 부분. : EachData

지금까지 눈에 보이는 UI를 기반으로 모두 박스모델을 만들었고 서로 같은, 단 하나의 기능을 하는 박스끼리 묶어서 컴포넌트로 만들었고 각각의 컴포넌트에 이름을 붙였다.

박스모델을 만들어 박스들을 토대로 컴포넌트들로 생각하는 과정이다.

다시해보면
1. 눈에 보이는 모든 기능에 대해 박스로 생각해라
2. 박스들끼리 동일한 기능을 한다면 하나의 컴포넌트로 생각해라
3. 컴포넌트가 하나의 기능만을 수행하는게 아니라 여러 기능을 수행한다면 여러 하위컴포넌트들로 다시 나눌 준비를 해라(단일 챔임 원칙에 따라서 컴포넌트를 생각하자)

참고로 DataContainer컴포넌트에서 Name Price헤더 부분을 박스로 분리하지도 않고 컴포넌트로 생각하지도않고 그냥 DataContainer컴포넌트 내부에 존재하게 했는데 이렇게 본인이 굳이 컴포넌트화 하지 않아도 될 부분에 대해선 이렇게 컴포넌트화 하지 않아도된다.
만약 이 헤더부분이 더 복잡해진다면 컴포넌트화 하는 것이 필요할수도 있겠다.

우리가 만든 컴포넌트들을 계층구조로 생각해보면
(계층 구조란 부모와 자식 컴포넌트를 설정하는과정이다.)

-	Container
-		UserInput
-		DataContainer
-			DataHeader
-			EachData

이런식으로 계층구조를 나눌수 있겠다.

그럼 여기까지다. 1단계는

  • React로 앱을 설계하는 첫번째 단계는 눈에 보이는 UI를 토대로 컴포넌트를 계층 구조로 나누는 것이다.
  • 컴포넌트로 분리하기 위해선 UI로 보이는 모든 부분을 박스 모델로 분리해야한다.
  • 박스 모델로 분리하고 단일 책임 원칙에 따라 박스들을 컴포넌트로 생각한다.
  • 만든 컴포넌트들을 자식 부모 컴포넌트 구성을 위해 계층 구조로 생각한다.

2단계: React로 정적인 버전 만들기


만들어진 컴포넌트 계층 구조를 토대로 앱을 실제로 만들어보는 단계이다.
일단은 동적인 부분은 모두 제거하고 정적인 부분을 만드는 것이다.
정적인 부분만을 만든다는 것은 state를 생각하지 않는 것이다.(state는 오직 사용자와의 상호작용을 위해, 시간이 지남에 따라 데이터가 바뀌는 것에 사용한다. 왜냐면 state는 setState가 일어나면 리렌더링이 일어나닌까.!)
즉, 컴포넌트만 만들고 컴포넌트간 데이터를 전달하는 props까지만 만드는 것이다.

그럼 만들어보자.

import ReactDOM from "react-dom";
import React from "react";

class Container extends React.Component {
  render() {
    return (
      <>
        <UserInput />
        <DataContainer datas={this.props.datas} />
      </>
    );
  }
}

class UserInput extends React.Component {
  render() {
    return (
      <div>
        <input type="text" placeholder="Serach..." />
        <br />
        <label>
          <input type="checkbox" />
          Onlyshow products in stock
        </label>
      </div>
    );
  }
}

class DataContainer extends React.Component {
  render() {
    const rows = [];
    let lastCategory = null;

    this.props.datas.forEach((data) => {
      if (data.category !== lastCategory) {
        rows.push(<DataHeader category={data.category} key={data.category} />);
      }
      rows.push(<EachData data={data} key={data.name} />);
      lastCategory = data.category;
    });
    return (
      <div>
        <header>Name Price</header>
        {rows}
      </div>
    );
  }
}

class DataHeader extends React.Component {
  render() {
    return <header>{this.props.category}</header>;
  }
}

class EachData extends React.Component {
  render() {
    return (
      <div>
        <span style={{ color: !this.props.data.stocked && "red" }}>
          {this.props.data.name}
        </span>
        <span> {this.props.data.price}</span>
      </div>
    );
  }
}

const JSONDATAS = [
  {
    category: "Sporting Goods",
    price: "$49.99",
    stocked: true,
    name: "Football"
  },
  {
    category: "Sporting Goods",
    price: "$9.99",
    stocked: true,
    name: "Baseball"
  },
  {
    category: "Sporting Goods",
    price: "$29.99",
    stocked: false,
    name: "Basketball"
  },
  {
    category: "Electronics",
    price: "$99.99",
    stocked: true,
    name: "iPod Touch"
  },
  {
    category: "Electronics",
    price: "$399.99",
    stocked: false,
    name: "iPhone 5"
  },
  { category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7" }
];
const rootElement = document.getElementById("root");

ReactDOM.render(<Container datas={JSONDATAS} />, rootElement);

JSONDATAS같은 경우는 뷰와 관련된 로직이 아니므로 의존성 주입을 했다.

사용자가 어떠한 입력을 가하던 간에 변화가 없는 정적인 버전이다.
이는 컴포넌트간의 계층 구조를 설정하고 설정한 부모, 자식 컴포넌트에 맞게 설정하고 데이터 같은 경우는 props를 이용해서 자식컴포넌트에 전달한 정적인 버전이다.

  • 설정한 컴포넌트 계층구조를 기반으로 state는 이용하지않고 props를 이용해서 정적인 앱 만들기

3단계: UI state에대한 최소한의 표현 찾아내기


정적인 앱은 다만들었으니 이제 사용자의 상호작용에 동작할 동적인 앱을 만들자.
동적인 앱을 만들기 위해선 state를 이용해야하는데 어떠한 데이터를 state로 할지 찾아내는 단계이다.

컴포넌트를 찾아낼때 하나의 기능만을 수행해야 한다는 단일 책임 원칙을 이용했다면 state를 찾아낼 땐 중복 배제 원칙을 이용한다.
중복 배제 원칙이란 어플리케이션이 필요로 하는 가장 최소한의 데이터를 state로 하고 그외의 데이터에 대해선 최소한의 데이터인 state에서 계산해서 중복을 없애자는 것이다.

공식문서에서 알려주는 예시에선 todolist의 state로는 단순히 todo들을 저장할 배열만 state로 있으면된다. 그외의 todo의 갯수등은 최소한의 데이터인 배열의 길이를 리턴하면되므로 따로 todo길이를 state로 정의할 필요는 없다.
즉, 최소한의 변경될 가능성이있는 데이터만 state로 존재하게 하자

우리가 만드려는 어플리케이션의 데이터를 정리해보면

  1. JSONDATAS(제품의 원본 데이터들)
  2. 유저가 입력한 검색어
  3. 유저가 입력한 체크박스 여부
  4. 필터링된 데이터 값

이중 어떤게 state가 되어야할까?

이 답변은 세가지 질문을 통해서 구할수 있다.
1. 부모로부터 props를 통해 전달됩니까? 그러면 확실히 state가 아니다.
2. 시간이 지나도 변하지 않나요? 그러면 확실히 state가 아니다.
3. 컴포넌트안의 다른 state나 props를 통해서 계산이 가능한 데이터인가요? 그렇다면 state가 아닙니다.

state는 setState를 통해 state인 데이터가 업데이트가 된다면 변경된 데이터에 맞게 다시 랜더링이 이루어 진다.
즉, 사용자의 상호작용에 따라 UI에 영향을 끼칠 변경이 될수 있는 데이터를 state라 한다.

이 개념을 토대로 생각해보면
1질문은 이미 부모로부터 받은 props를 굳이 state로 둘 필요가 있을까?를 생각해보면 그렇지 않다고 결론이 지어진다.
부모로부터 받은 props나 변경이될 데이터라면 부모에서 전달한 props를 state로 정의를 하면되는 것이다.
굳이 자식에서 한번더 state로 정의할 필요가 없다.
2질문은 state가 뭔지에 대해 생각하면 쉽게 결론이 나올수 있다.
3질문은 state를 정의할때 위에서 얘기했던 중복 배제 원칙을 생각하면 된다.

이런식으로 사용자의 상호작용에 따라 (UI에 영향을 주는) 동적으로 변경될수 있는 데이터인 state를 생각할수있다.

우리 어플에서 state로써 존재할수 있는 녀석들을 생각해보자

  • JSONDATAS(원본 데이터들)
    1질문인 props로 받은 데이터이므로 state 탈락
  • 유저가 입력한 검색어
    props로 받는 데이터도 아니고 동적으로 변경될 데이터이고 다른 state나 props로부터 계산될 데이터도아니므로 state당첨
  • 유저가 입력한 체크박스 여부
    이 부분도 유저가 입력한 검색어와 동일하다는 걸 알수있다.
  • 필터링된 데이터 값
    원본 데이터들로부터 (props로 받는) 유저가 입력한 검색어(state), 유저가 입력한 체크박스 여부(state)의 계산으로 나올수 있는 값이므로 state아니다.

즉, 우리가 설정해야할 state는 유저가 입력한 검색어, 유저가 입력한 체크박스 여부가 되겠다.

4단계: State가 어디에 있어야 할 지 찾기


지금까지 어떤 state가 있어야할지 찾았다.
state는 컴포넌트 내부에있는 캡슐화된 데이터인데 어떤 컴포넌트에 있어야할지 이제 생각해보자.

이 부분도 적절한 질문에 따라 답하면서 정답을 찾아보자.

  1. state를 기반으로 렌더링하는 모든 컴포넌트를 찾으세요
    ->사용자가 입력한 검색어와 체크박스에 따라 DataContainer컴포넌트 가 참조하는 DataHeader컴포넌트와 EachData가 렌더링 되겠다, 그리고 우린 제어 컴포넌트를 이용할 것이므로 UserInput컴포넌트도 state에 따라 렌더링 되는 컴포넌트이다.

  2. 공통 소유 컴포넌트를 찾으세요.
    ->Container컴포넌트가 공통 소유컴포넌트이다.

  3. 공통 혹은 더 상위에 있는 컴포넌트가 state를 가져야 한다.
    ->우리는 Container컴포넌트에서 state가 있으면 되겠다.

-	Container
-		UserInput
-		DataContainer
-			DataHeader
-			EachData

state들 공통소유컴포넌트인 Container컴포넌트에서 state에 위치시키자.
이는 React가 단방향 데이터흐름이므로 이런방식으로 state를 위치 시키는 것이다.
(State는 props를 통해서만 자식컴포넌트로 전달될수 있다. state는 해당 컴포넌트외엔 접근 불가.)

  1. state를 소유할 적절한 컴포넌트가 없다면 state를 소유하는 컴포넌트를 하나 만들어서 공통 소유 컴포넌트의 상위 계층에 추가하라.
    -> 우리는 적절한 Container컴포넌트가 있으므로 해당되지 않는다. 만약 적절한 컴포넌트없으면 임의로 공통소유 컴포넌트 상위컴포넌트를 만들어서 state를 가지고 있도록 하라는 뜻이다.

그럼 이제 state를 추가해보자.

import ReactDOM from "react-dom";
import React from "react";

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: "",
      isStock: false
    };
  }
  render() {
    return (
      <>
        <UserInput
          filterText={this.state.filterText}
          isStock={this.state.isStock}
        />
        <DataContainer
          datas={this.props.datas}
          filterText={this.state.filterText}
          isStock={this.state.isStock}
        />
      </>
    );
  }
}

class UserInput extends React.Component {
  render() {
    return (
      <div>
        <input
          type="text"
          placeholder="Serach..."
          value={this.props.filterText}
        />
        <br />
        <label>
          <input type="checkbox" value={this.props.isStock} />
          Onlyshow products in stock
        </label>
      </div>
    );
  }
}

class DataContainer extends React.Component {
  render() {
    const rows = [];
    let lastCategory = null;

    this.props.datas.forEach((data) => {
      if (data.name.indexOf(this.props.filterText) === -1) return;
      if (this.props.isStock && !data.stocked) return;
      if (data.category !== lastCategory) {
        rows.push(<DataHeader category={data.category} key={data.category} />);
      }

      rows.push(<EachData data={data} key={data.name} />);
      lastCategory = data.category;
    });

    return (
      <div>
        <header>Name Price</header>
        {rows}
      </div>
    );
  }
}

class DataHeader extends React.Component {
  render() {
    return <header>{this.props.category}</header>;
  }
}

class EachData extends React.Component {
  render() {
    return (
      <div>
        <span style={{ color: !this.props.data.stocked && "red" }}>
          {this.props.data.name}
        </span>
        <span> {this.props.data.price}</span>
      </div>
    );
  }
}

const JSONDATAS = [
  {
    category: "Sporting Goods",
    price: "$49.99",
    stocked: true,
    name: "Football"
  },
  {
    category: "Sporting Goods",
    price: "$9.99",
    stocked: true,
    name: "Baseball"
  },
  {
    category: "Sporting Goods",
    price: "$29.99",
    stocked: false,
    name: "Basketball"
  },
  {
    category: "Electronics",
    price: "$99.99",
    stocked: true,
    name: "iPod Touch"
  },
  {
    category: "Electronics",
    price: "$399.99",
    stocked: false,
    name: "iPhone 5"
  },
  { category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7" }
];
const rootElement = document.getElementById("root");

ReactDOM.render(<Container datas={JSONDATAS} />, rootElement);

Container컴포넌트에 state를 추가하고 이 진리의 원천인 state에서 props로 각각 전달했다.

  • state를 어떤 컴포넌트에 위치시킬지는 이 state가 변경됨에 따라 어떤 컴포넌트들이 렌더링 될지 찾는게 먼저이다.
  • 그런다음 공통소유 컴포넌트를 찾는다.
  • 공통 소유 컴포넌트를 찾은뒤 해당 컴포넌트거나 상위컴포넌트에 state를 위치한다.
  • 이렇게 공통소유 컴포넌트, 상위컴포넌트에 state를 위치시키는 이유는 React는 단방향 데이터흐름으로 state는 props로써 전달되고 이를 위해선 하나의 진리의 원천인 state를 두는 것이 관리가 용이하기 때문이다.

5단계: 역방향 데이터 흐름 추가하기


우리는 지금까지 단방향 데이터흐름으로 state가 props로 전달되는 어플리케이션을 만들었다.
이제는 다른방향의 데이터 흐름을 만들어야한다.
UserInput컴포넌트에서 입력되는 값에 맞게 그 상위컴포넌트인(state가 존재하는 컴포넌트) Container컴포넌트에서 state가 업데이트 되어야한다.

이는 양방향 데이터흐름이 아닌것에 주의하자. 단순히 계층면에서 하위컴포넌트에서 상위컴포넌트의 데이터를 업데이트 시키는거지 상위컴포넌트로 데이터를 전달하는 것은 아니다.
즉, 단방향 데이터 흐름을 위한것으로 하위컴포넌트에서 상위컴포넌트의 데이터를 업데이트 시키는 것이다.

이는 조금 귀찮지만 단방향데이터흐름을 지킴으로써 프로그램이 어떤식으로 동작하는지 파악할수 있게 해준다.

이 방법은 제어컴포넌트에서 배웠던 부분과 state lift up에서 배웠던 부분을 이용해서 쉽게 구현 가능하다.

import ReactDOM from "react-dom";
import React from "react";

class Container extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: "",
      isStock: false
    };
  }
  inputText = (e) => {
    const value = e.target.value;
    this.setState({ filterText: value });
  };
  inputCheck = () => {
    this.setState({ isStock: !this.state.isStock });
  };
  render() {
    return (
      <>
        <UserInput
          filterText={this.state.filterText}
          isStock={this.state.isStock}
          changeHandler={this.inputText}
          changeFilter={this.inputCheck}
        />
        <DataContainer
          datas={this.props.datas}
          filterText={this.state.filterText}
          isStock={this.state.isStock}
        />
      </>
    );
  }
}

class UserInput extends React.Component {
  render() {
    return (
      <div>
        <input
          type="text"
          placeholder="Serach..."
          value={this.props.filterText}
          onChange={this.props.changeHandler}
        />
        <br />
        <label>
          <input
            type="checkbox"
            value={this.props.isStock}
            onChange={this.props.changeFilter}
          />
          Onlyshow products in stock
        </label>
      </div>
    );
  }
}

class DataContainer extends React.Component {
  render() {
    const rows = [];
    let lastCategory = null;

    this.props.datas.forEach((data) => {
      if (data.name.indexOf(this.props.filterText) === -1) return;
      if (this.props.isStock && !data.stocked) return;
      if (data.category !== lastCategory) {
        rows.push(<DataHeader category={data.category} key={data.category} />);
      }

      rows.push(<EachData data={data} key={data.name} />);
      lastCategory = data.category;
    });

    return (
      <div>
        <header>Name Price</header>
        {rows}
      </div>
    );
  }
}

class DataHeader extends React.Component {
  render() {
    return <header>{this.props.category}</header>;
  }
}

class EachData extends React.Component {
  render() {
    return (
      <div>
        <span style={{ color: !this.props.data.stocked && "red" }}>
          {this.props.data.name}
        </span>
        <span> {this.props.data.price}</span>
      </div>
    );
  }
}

const JSONDATAS = [
  {
    category: "Sporting Goods",
    price: "$49.99",
    stocked: true,
    name: "Football"
  },
  {
    category: "Sporting Goods",
    price: "$9.99",
    stocked: true,
    name: "Baseball"
  },
  {
    category: "Sporting Goods",
    price: "$29.99",
    stocked: false,
    name: "Basketball"
  },
  {
    category: "Electronics",
    price: "$99.99",
    stocked: true,
    name: "iPod Touch"
  },
  {
    category: "Electronics",
    price: "$399.99",
    stocked: false,
    name: "iPhone 5"
  },
  { category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7" }
];
const rootElement = document.getElementById("root");

ReactDOM.render(<Container datas={JSONDATAS} />, rootElement);

우리는 진리의 원천인 state를 가지고있는 Container컴포넌트의 state를 업데이트 시키기 위해 props로 함수를 전달했고 이 콜백함수를 통해서 상위컴포넌트에서 state를 업데이트 시켰다.

이게 끝이다.

  • 역방향 데이터 흐름을 추가해서 상위 컴포넌트의 state를 업데이트 시켰다. 이는 진리의 원천인 state를 상위컴포넌트에서 하나만 가지게 하기 위함이고 이를 통해 단방향 데이터흐름을 지키기 위함이다.

이번 주제의 느낀점

  • 박스 모델을 통해서 컴포넌트를 계층화 하기

눈에 보이는 UI에 따라 설계하는건 되게 좋은 것 같다. UI에따라 프로그램을 설계하고 UI 맞게 다양한 로직들도 세우는 것은 무언가를 개발할때 직관적으로 개발할수 있게 해주기 때문에 되게 좋다 생각한다.
그치만 요새는 컴포넌트계층화할때 분명 공통의 기능이라 하나의 컴포넌트로 둘수 있을 거같은데 뭔가 하나의 컴포넌트로 두면 더 복잡해질거 같다는 생각이 들곤한다. 이럴때 조금 힘들다. 복잡성을 낮추기 위해 공통의 기능이지만 다른 컴포넌트로 정의할지, 조금 복잡하지만 공통의 기능이므로 같은 컴포넌트로 정의할지. 후자가 적절하다 보지만 그래도 나는 이부분이 개발할때 가장 고민되는 부분이다.

  • 정적인 버전 만들기

이 단계는 컴포넌트계층화를 잘하면 어렵지 않은 부분이라 생각한다. 첫단추를 잘 꿰매야 뒷부분도 쉽듯이 컴포넌트 계층화를 잘 해놓으면 정적인 버전도 만들기 쉽다 생각한다. 그치만 정적인 버전을 만들다가 계층화가 잘못된거같으면 다시 하면 되기 때문에 문제는 없다 생각한다.

  • 최소한의 UI state 생각하기

이 부분도 굉장히 고민되는 부분이다. 저번에 포스팅한 부분이 이부분이였는데 그때는 중복 배제 원칙을 몰라서 생긴 문제였다. state는 최소한으로 그치만 상호작용에 의해 변경될수 있는 데이터여야 한다!

  • state가 위치할 컴포넌트 고르기

하나의 데이터 변경에 따른 여러 컴포넌트의 변화가 있을 경우 우리는 공통의 조상(부모)컴포넌트에 state를 위치시켰다. state를 자식컴포넌트로 props로 전달할수 있기 때문이고 하나의 데이터 변화이므로 하나의 데이터 변화를 여러 컴포넌트에서 참조하기 위해선 공통의 조상이여야 하기 때문이다. 이를 토대로 state가 변경됨에 따라 랜더링 될 컴포넌트들을 찾고 공통의 컴포넌트 혹은 그상위 컴포넌트에 state를 위치시키면된다.
React는 단방향 데이터흐름임을 잊지말자.

  • 역방향 흐름 추가하기

이는 단방향 데이터흐름을 위한것이다. 복잡하다 생각할수도 있지만 React는 단방향 데이터흐름으로 여러가지 장점을 제공한다. 프로그램이 돌아가는 로직을 파악하기 쉽게하고, 오류찾기도 쉽고.. 이를 위해서 자식 컴포넌트에서 상위컴포넌트(진리의 원천이 존재하는)의 state를 업데이트 시킬수 있어야한다. 이 부분을 통해 단방향 데이터흐름을 확고히 할수 있다. 처음엔 어려웠지만 콜백함수를 통해서 역방향 데이터흐름을 추가하는 것은 하면 할수록 익숙해 지는 것같다.

추가로 이러한 기본을 모른채로 나는 useIperativeHandler이라던가 역방향으로 데이터를 전달하곤했다.
보다더 기본을 충실히하고 심화에 들어갈수 있도록 하자.

0개의 댓글