본문 바로가기
Language/React

[React 공식 Doc 가이드 #12] Thinking in React

by ocwokocw 2021. 2. 11.

- 이 글은 React 공식 홈페이지 Docs v16.8.3 (https://reactjs.org/docs) 에 기반하여 작성한 글입니다.

- Thinking in React

React는 JavaScript 를 사용하는 대규모 Web app 일 빌드하고, 빠른 Web app 을 만들기 위한 최고의 방법이다. Facebook 과 Instagram 과 같은 큰 규모의 앱 제작에서도 사용되었다.

 

React는 마치 프로그래머가 app 을 빌드하는 것처럼 app 에 대해 생각하게 만든다는 점이다.무슨말인지 이해가 가지 않을 것이다. 이번 문서에서는 React를 이용해서 검색을 할 수 있는 상품 테이블을 만드는 과정을 통해 위의 문장의 의미를 알아볼 것이다.


- Start With A Mock

서버에 JSON API 가 구축되어있고, 디자이너로 부터 mock(디자인에서 mock 이라면 임시적으로 만든 프로토타입성 디자인을 말하는듯, JUnit 테스트 글을 본 사람이라면 들어봤을 것이다.) 을 받았다고 가정하자.

 

mock은 아래와 같다.

JSON API 에서 return 해주는 data 는 아래와 같다.

 

{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"}
];

- Step 1: Break The UI Into A Component Hierarchy

소제목에서도 알 수 있듯이 젤 먼저 해야할 일은 mock 에서 Component 라고 생각되는 부분을 box 로 그려보고 각 box 마다 이름을 붙여주는 것이다. 만약 designer와 같이 일하고 있다면 이미 이 작업을 했을 수도 있다. 디자이너가 정의한 Photoshop 레이어 이름을 React Component 의 이름과 맵핑 시키자.

 

위에 설명한대로라면 테크닉적으로는 크게 어려운 점은 없다. 그런데 맵핑시키는 테크닉이 중요한게 아니다. 한 가지 의문점은 내가 생각해서 box를 그린부분이 정말 Component 단위로서 적절한지를 어떻게 알 수 있느냐이다. 공식 문서에서는 함수나 Object 를 만들때와 같은 원칙을 적용하라고 말한다.

 

그래서 그게 뭐냐고 묻는다면 그 유명한 Single responsibility principle 이다. 이 글을 읽는 여러분이 마틴 파울러를 안다던가 객체지향 설계의 5대 원칙(SOLID)를 알고 있다면 이 뜻을 이미 알 고 있을 것이다. 모른다고 해도 만약 단일 책임 원칙을 읽을 시간이 없거나 귀찮다면 대전제 한 가지만 기억하면 된다. 1가지 Component는 1가지의 기능만 해야 한다.

위 원칙을 적용하려고 하면 자연스럽게 App의 규모가 커질수록 Component를 더 작은 단위로 쪼개야 한다는 생각이 자연스럽게 들 것이다.

 

JSON Data model을 유저에게 보여줄 때, 모델이 잘 설계되었다면 UI(Component 구조)가 모델과 잘 맵핑 되는지 확인해야 한다. UI와 data model 들은 같은 정보 구조를 가지려는 경향이 있기 때문에, UI를 Component 로 쪼개는 작업은 그리 힘들지 않다. 단지 Component가 data model의 한 부분을 정확하게 표현할 수 있게 쪼개면 된다.

이 예제에서는 5개의 Component로 나누었다. 아래 문구중 []는 각 Component가 표현하는 data를 나타낸다.

  1. FilterableProductTable(주황색): 이 예제의 전체 Component
  2. SearchBar(파랑): [user input] 부분
  3. ProductTable(초록): [user input]에 기반하여 필터링된 [data collection] 을 표시하는 부분
  4. ProductCategoryRow(청록색): 각 [category]를 표시한 부분
  5. ProductRow(빨간색): 각 [product]의 행을 표시한 부분

 

3.의 ProductTable(초록) 부분을 보면 "Name", "Price"에 대한 table header는 자신의 Component가 없다. 이건 개인취향의 문제인데 이 예제에서는 argument 를 통해서 해당부분의 역할을 할 수 있다고 봤기 때문이다.

 

이 예제에서는 ProductTable의 책임인 [data collection]을 rendering하는데 부가적인 부분이라고만 보았기 때문이다.

 

그러나 만약 header가 더 많은 기능을 가지게 된다면, 이 부분을 ProductTableHeader와 같은 Component로 구성할 수 있을 것이다.

 

이제 mock에서 component를 모두 찾아냈다. 이제 이 Component를 계층구조(Tree 구조)로 바꿔보자. 이건 크게 어렵지 않다. mock 에서 하나의 Component는 다른 Component 안에 포함되어있기 때문에 이 구조를 그대로 적용하면 된다.


- Step2: Build A Static Version in React

Component 계층구조를 완성했으면 이제 구현할 차례다.

 

일단 처음엔 Component간에 상호작용같은걸 고려하지말고 data model과 UI의 rendering 부분만을 고려해서 코딩하는게 낫다.

 

static version(Component 간에 상호작용이 없는 version)은 타이핑은 많지만 생각을 많이 할 부분은 없기 때문이다. static version이 완성되면 여기에 Component간의 상호작용을 더하는데 이때는 타이핑은 별로 없지만 상당히 많은 생각을 해야한다.왜 이 순서로 작업해야 하는지 살펴보자.

 

설계한 data 모델을 rendering하는 app의 static version을 만들 때, 다른 Component들을 재사용하고 props를 이용해서 data를 넘긴 Component를 사용한 형태로 빌드하길 원할것이다.

 

props를 이용하면 부모에서 자식으로 data를 넘길 수 있다. 이미 React에 대해 잘 알아서 state를 잘 사용한다고 해도 static version을 만들떄는 state를 사용하면 안된다.

 

state란 시간이 흘러가면서 변할 수 있는 data를 표현하는 기능이기 때문에 static version을 만들 때는 state를 사용할 필요는 없다.

 

static version을 만들 때는 top-down 방식과 bottom-up 방식이 있다. 우리가 만든 예제에서 보자면 FilterableProductTable 부터 혹은 ProductRow 부터 만들어도 된다는 이야기이다.

 

일반적으로 큰 Project 에서 구현할때에는 top-down 방식이 수월하며, test를 작성할 때에는 bottom-up 이 편할것이다. JUnit 으로 테스트 케이스를 작성해본 사람은 알겠지만 일단 세부적인 기능의 테스트케이스를 만족 시키고, 큰 기능이 돌아가는지 확인한다는걸 생각하면 무슨말인지 알 것이다.

 

이 step의 마지막 단계에서는 설계한 data model을 rendering 하는 기능을 가진 Component를 완성하게 될 것이며, 해당 Component는 재사용 가능한 library가 될 것이다.

 

static version이기 때문에 Component들은 render() 메소드만 가진 형태가 된다. 현재 최상위Component(FilterableProductTable)는 data를 prop을 통해 전달받고 있을 것이다. 지금은 data model을 변경하고 ReactDOM.render()를 호출하면 UI가 변경 될 것이다. 자동으로 변경되지는 않지만 현재 상태는 state를 쓰지않아서 복잡하지 않아서 어떻게 UI가 변경되는지 어디에서 변화가 일어나는지 파악하기에 좋다. React의 one-way data flow(one-way binding)방식은 app을 modular 형태로 구성할 수 있게 해주며, 속도도 빠르게 해준다.

 

막간 요약: Props vs State

React에는 2가지 타입(props와 state)의 "model" data가 있다. 2가지의 차이점을 꼭 이해해야 한다. 아직 2가지 차이를 이해하지 못하고 있따면 -State and Lifecycle을 다시 읽어보길 바란다.


- Step 3: Identify The Minimal (but complete) Representation Of UI State

반응형 UI를 만들때에는 data model에 따라 UI가 변하는 방식으로 구성해야 한다. (MVVM 패턴을 안다면 이해가 쉬울 것이다.) React에서는 state로 손쉽게 구현할 수 있다. 공식문서에서는 손쉽다고 하지만 구현은 쉬우나 생각을 많이 해야 한다.

 

app을 제대로 만들려면 최우선적으로 생각해야 할 것이있다. app이 필요한 mutable state(상태가 변할 수 있는)의 최소단위에 대해 생각해 봐야 한다.

 

가장 중요한 키 포인트는 DRY(Don'y Reapeat Yourself) 원칙이다.(DRY : https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)

 

공식문서의 번역이 꽤나 어렵지만 예제를 보면 무슨말인지 쉽게 알 수 있다.

  1. 우리가 설계한 app의 요구사항을 모두 만족한다.
  2. app에 필요한 모든 정보가 있다. 
  3. 그 외의 나머지 정보는 이미 가지고 있는 정보를 이용해서 알 수 있다.

위의 3가지 조건을 만족하는 state를 식별하는 것이 중요하다. 예를 들어보자. 우리가 만약 TODO list app을 만들고 있다. 여기서 TODO의 총 개수를 표시해야 하는 요구사항이 있다고 치자. 그럼 위의 3가지 조건을 만족하는 state는 {todoList: [1,2,3], count: 3}일까? 이게 최소정보로 표현한 상태일까? 그렇지 않다. TODO의 총 개수는 todoList.length를 통해 알 수 있으므로 {todoList: [1,2,3]}를 state로 인식해도 된다. (물론 SI 플젝이나 회사에서 플젝을 뛰어본 사람이라면 현실의 문제는 훨씬 더 복잡하고, 위의 예제가 극단적인 예시라는걸 알 것이다. 그래도 생각하려고 노력이라도 하자.)

 

우리 예제 app에서 필요한 data를 통째로 생각하지 말고 한번 나눠보자.

  1. products의 목록
  2. user가 입력할 수 있는 검색 조건
  3. checkbox의 value
  4. 필터링된 products

위의 4가지 중에 어떤게 진짜 state 인지를 판별해보자. 각 data 마다 아래 3가지에 대해서 생각해보면 된다.

  1. 부모로부터 props를 통해 받는 data 인가?
  2. 시간의 흐름에 관계 없이 변하지 않는 data 인가?
  3. Component내에서 다른 state나 props로 부터 도출될 수 있는 data 인가?

위의 3가지 조건 중 하나라도 만족했다면 state 에서 배제 해야 한다.

 

products의 original list는 props를 통해서 전달받으므로 state가 아니다. search text나 checkbox는 시간이 지남에 따라 변경가능성이 있고, 다른 요소로부터 도출될 수 있는 data가 아니라서 state 이다. 마지막으로 filtering된 products는 original list + 검색조건 + 체크박스의 조합으로 도출가능하므로 state가 아니다.

 

최종적으로 우리가 state라고 판단할 수 있는건 아래 2가지 이다.

  1. 유저가 입력할 수 있는 검색조건
  2. checkbox의 value

- Step 4: Identify Where Your State Should Live

지금까지 app state가 가져야할 최소한의 정보를 판별했다. 이제 변할 가능성이 있는 Component인지 아니면 자기자신으로서 가치가 있는 Component 인지 판단해야 한다. (Remember: React는 하위전파로만 data가 전파되는 계층구조이다.)

 

아마 어떤 Component가 state를 가져야 하는지 명확히 와닿지가 않을 것이다. 이부분은 React 입문자들에게 가장 어려운 부분이기도 한데, 아래 step을 차례로 밟아가면서 이해하도록 해보자.

 

우리가 찾은 state 2개에 대해서 각각 아래 규칙을 적용해보자 (1. state에 대해 1번, 2.state에 대해 1번)

  1. state 값에 의존해서 rendering 하고 있는 Component를 모두 식별한다.
  2. 식별한 Component의 공통 조상 Component를 찾자. (계층구조에서 state를 필요로 하는 Component 들이 있을건데, 이 Component들을 모두 포함할 수 있는 Component를 찾자.) (속된말로 최소 공통조상을 찾으라는 말인 것 같다. 검색해서 그림으로 보면 바로 이해할 수 있다.)
  3. 2.에서 찾은 공통 조상 Component와 다른 공통 조상 Component중 더 위에 있는 Component가 state를 가져야 한다.
  4. 만약 state를 가질만한 Component를 찾을 수 없으면, 새로운 Component를 만든다.

그리고 common owner component 위 어딘가에 추가하자. 위의 규칙을 우리 예제에 적용시켜보자.

  1. ProductTable 은 state 값에 기반해서 product list를 filtering 한다. 그리고 SearchBar 는 seacrh text와 checked 된 상태를 표시하므로 이것도 state값에 의해 rendering 되는 Component 이다.
  2. ProductTable과 SearchBar의 공통조상은 FilterableProductTable 이다.
  3. state는 FilterableProductTable 안에 있어야 한다.

state가 있어야 할 Component가 결정 됐다. 우선 FilterableProductTable 생성자에서 this.state에 filterText와 inStockOnly 속성을 초기화 하자. 그리고 filterText와 inStockOnly를 ProductTable과 SearchBar에 prop으로 전달한다. filterText로 ProductTable의 행을 필터링하고, inStockOnly로 SearchBar의 체크박스 value를 세팅한다.

 

이제 앱의 동작과정을 볼 수 있다. filterText에 "ball"을 입력하고 새로고침 해보자. data table이 갱신되는걸 볼 수 있을 것이다.


- Step 5: Add Inverse Data Flow

지금까지 props의 함수와 state를 하위전파특성을 이용하여 app을 구성하였다. 하지만 아직 코딩하지 않은 data binding이 있다. 사용자가 검색조건을 변경할 때 FilterableProductTable의 state를 갱신하지 않았다. 혹시 Vue를 써본적이 있는 사람이 있는지 모르겠다. Deep dive를 해본적이 없다 해도 예제 초반부의 two-way binding을 보며 신기했을 것이다. 나도그랬다. (two-way binding: 간단히 예를 들면 별다른 설정없어도, model을 변경하면 input박스에 표시되고 input박스에 값을 입력하면 해당값으로 model에 자동반영됨.)

 

하지만 React는 one-way binding 이라서 input박스를 입력했을 때, state에 대한 반영부분을 코딩해줘야 한다.(앞선 챕터에서 onChange 이벤트내부에 setState 로 state 를 변경했던 것을 기억해보자.) 귀찮을 수도 있겠으나 이런점이 무조건 단점은 아니다. 사실 이해하기에는 one-way 바인딩이 더 명확하기 때문이다.

 

현재 버전에서는 box를 체크하거나 검색어를 입력해도 반응이 일어나지 않는다. 코딩을 잘못한게 아니라 아직 input을 FilterableProductTable의 state와 연동시키지 않았기 때문이다.

 

우리가 뭘 원하는지 잘 생각해보자. 사용자가 form을 변경할 때마다 해당값이 state에 반영되길 원한다. Component들은 자기가 가지고 있는 state만 갱신하기 때문에, SearchBar Component에 callback함수를 넘기고, SearchBar Component 안의 사용자 입력 부분에서는 입력할 때 마다 넘겨받은 callback 함수를 실행시켜서 부모의 state를 변경해야 한다.

앞부분에서 이미 배운적이 있다. onChange 이벤트를 만들고, FilterableProductTable에서 넘겨받은 함수를 호출하면, 해당 함수내부에서는 setState()를 호출하고, app이 갱신될 것이다. 복잡하게 들리지만 code로는 몇줄 되지 않는다.


- And That's It

지금까지 React를 사용한 app에서 어떻게 Component를 설계하는지 알아보았다. React없이 그냥 구현하는거에 비해 타이핑 양이 많지만 기억해야 할 것이 있다. 코드는 쓰여지는 것 보다 읽힐때가 더 많고, modular 방식이 가독성도 좋고 명료하다. 토이 Project가 아닌 실전에서 modular 방식으로 구현해본다면 장점을 이해할 수 있을것이다.

 

댓글