들어가기 전에

리액트 공식문서로 배워보자 #12, 리액트처럼 생각하기

우리의 의견으로는, 리액트는 자바스크립트로 크고 빠른 앱을 빌드하기 위한 최고의 방법입니다. 페이스북과 인스타그램에서 우리에게 매우 좋은 확장성(scalability)도 보여주고 있습니다.

리액트의 많은 좋은 부분 중 하나는 그것들을 빌드하면서 앱에 대해 어떻게 생각하게 만드느냐 입니다. 이 문서에서, 우리는 리액트를 사용한 검색이 가능한 제품 데이터 테이블을 구축하는 것의 사고 과정 대해 알아볼 것입니다.

샘플 디자인으로 시작하기

우리가 이미 JSON API를 갖고 있고 우리 디자이너로부터 샘플 디자인을 받았다고 상상해봅시다. 샘플 디자인은 아마 다음과 같이 생겼을 겁니다.

mock.png

우리 JSON API는 아래처럼 생긴 데이터를 반환합니다.

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

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

처음으로 여러분이 해야 할 것은 샘플 디자인에서 모든 컴포넌트들 주변에 박스를 그리는 것입니다. 그리고 모두 이름을 지어줍시다. 디자이너와 함께 일하는 중이라면, 디자이너들이 이미 이 작업을 끝냈을지도 모릅니다. 그러니 가서 이야기를 나눠보세요! 디자이너가 포토샵 레이어에서 지정한 이름들이 결국 리액트 컴포넌트의 이름이 될 수 있습니다.

하지만, 어떤 것이 자체적인 컴포넌트가 될 것인지 어떻게 알까요? 그럴 땐 그냥 새로운 함수 또는 오브젝트를 만드는 것이 바람직한지 결정하는 것과 같은 기술을 사용하면 됩니다. 그러한 기술 중 하나는 '한 컴포넌트는 오직 하나의 작업만 하는 것이 이상적이다' 라는 단일 책임 원칙입니다. 만일 자꾸 커진다면, 더 작은 컴포넌트로 분리하는 것이 바람직합니다.

여러분이 종종 JSON 데이터 모델을 사용자에게 보여주기 때문에, 여러분은 여러분의 모델이 제대로 만들어졌는지, 컴포넌트 구조에 따라 UI가 제대로 예쁘게 맵핑 되는지 확인해봐야 합니다. UI와 데이터 모델은 동일한 인포메이션 아키텍쳐(information architecture) 를 고수하기 때문에, UI를 컴포넌트로 나누는 것은 종종 하찮은 일입니다. 그냥 UI를 여러분의 데이터 모델에서 정확히 한 부분을 표현하는 컴포넌트들로 나누세요.

uicomponent.png

여러분은 여기서 우리가 5개의 컴포넌트를 갖고 있다는 것을 알게될 것입니다. 우리는 각 컴포넌트가 표현하는 데이터를 이탤릭체로 보여줄 것입니다.

  1. FilterableProductTable (오렌지색) : 예제의 전체를 포함합니다.
  2. SearchBar (파란색) : 모든 사용자 입력 값을 받습니다.
  3. ProductTable (초록색) : 사용자 입력에 따른 데이터의 그룹을 보여주고 필터링합니다.
  4. ProductCategoryRow (옥색) : 각 카테고리의 제목을 보여줍니다.
  5. ProductRow (빨간색) : 각 제품에 대한 행을 보여줍니다.

여러분이 ProductTable을 본다면, 테이블 헤더가 ("Name"과 "Price" 레이블 포함) 자체적인 컴포넌트가 아닌 것을 알 수 있습니다. 이건 어떤 방식을 좋아하냐의 문제인데, 둘 중 어떤 것으로 만들어져야 하는지에 대한 논쟁이 있습니다. 이 예제에서, ProductTable의 책임 영역에 있는 데이터 그룹을 렌더링 하는 역할 일부로 보았기 때문에, 우리는 이걸 ProductTable의 일부로 남겨두었습니다. 하지만, 이 헤더가 더욱 복잡하게 커진다면 (이를테면, 우리가 정렬에 대한 기능을 추가한다면), 이 컴포넌트를 ProductTableHeader 라는 자체 컴포넌트로 만드는 것이 더욱 현명할 것입니다.

이제 우리는 샘플 디자인에서 컴포넌트들을 식별했습니다. 이제 식별한 컴포넌트들을 계층으로 배열해봅시다. 이건 쉽습니다. 샘플 디자인에서 다른 컴포넌트 내부에 나타나는 컴포넌트들은 계층에서 자식으로 표현되는 것이 바람직합니다.

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

단계 2: 리액트에 정적인 버전 빌드하기

코드펜에서 리액트처럼 생각하기: 단계 2를 보세요.

이제 여러분은 컴포넌트 계층을 가졌습니다. 이젠 앱을 구현할 시간입니다. 데이터모델을 가져가서 UI를 렌더링하는 것이 가장 쉽지만 사용자와 상호작용하는 것은 없을 것입니다. 이러한 절차들은 쪼개는 것이 가장 좋습니다 왜냐하면 정적인 버전을 만드는 것은 생각 없이 많은 타이핑만을 요구합니다. 그리고 상호작용을 추가하는 것은 많은 생각과 적은 타이핑을 요구합니다. 왜 인지 살펴봅시다.

데이터모델을 렌더링하는 여러분의 앱의 정적인 버전을 빌드하기 위해서, 여러분은 다른 컴포넌트들을 재사용하고 props를 이용하여 데이터를 넘기는 컴포넌트들을 빌드하길 원할 것입니다. props는 데이터를 부모에서 자식으로 보내는 방법 중 하나입니다. 만일 여러분이 state의 개념에 익숙하다면, 정적인 버전을 만들기 위해서는 state를 절대 사용하지 마세요. State는 데이터가 몇번이고 변경되는 상호작용성을 위해서 사용되는 것입니다. 앱의 정적인 버전을 만들기 때문에 state는 필요 없습니다.

여러분은 top-down 또는 bottom-up으로 빌드할 수 있습니다. 컴포넌트를 위 계층에서 아래로 향하며 빌드하며 시작하거나 (예를 들면, FilterableProductTable 부터 시작) 또는 아래 컴포넌트들로부터 위로 시작할 수도 있습니다. 간단한 예제에서는, top-down 방법이 쉬운 경우가 많습니다. 그리고 큰 프로젝트에서는, bottom-up 방법이 쉬운 경우가 더 많고 테스트도 작성하기 더 쉽습니다.

이 단계의 마지막에서, 여러분은 여러분의 데이터 모델을 렌더링하는 재사용 가능한 컴포넌트의 라이브러리를 갖게 될 것입니다. 이 라이브러리는 여러분의 앱의 정적인 버전이기 때문에, 컴포넌트는 오직 render() 메소드만 갖게될 것입니다. 가장 상위 계층의 컴포넌트(FilterableProductTable)은 여러분의 데이터 모델을 prop으로 가져갈 것입니다. 만일 여러분이 존재하는 데이터 모델에 변화를 가하고 ReactDOM.render() 메소드를 다시 호출하면, UI가 업데이트 될 것입니다. 여러분의 UI가 어떻게 업데이트 되는지 어디를 바꿔야 하는지 알아보는 것은 매우 쉽습니다. 왜냐하면 지금은 어떠한 복잡한 것도 없기 때문입니다. 리액트의 단방향 데이터 흐름(단방향 바인딩으로도 불림) 은 모든 것을 모듈화 시키고 빠른 상태로 유지합니다.

만일, 이 단계를 수행하는데 도움을 받고 싶다면, 간단히 React docs를 참조하세요.

잠시 살펴보기: Props vs State

리액트에는 2가지 종류의 "모델" 데이터가 존재합니다: props와 state. 두 개의 차이를 이해하는 것은 중요합니다. 차이를 잘 모르신다면 React 공식문서를 훑어보세요.

단계 3: UI 상태의 최소한도 표현 확인하기

여러분의 UI를 상호작용하게 만들기 위해, 존재하는 데이터 모델에 변화를 가할 수 있게 할 필요가 있습니다. 리액트는 state로 이러한 작업을 쉽게 수행할 수 있게 해줍니다.

앱을 올바르게 빌드하기 위해, 여러분의 앱이 필요로 하는 변화하는 상태의 최소한의 세팅을 생각해보아야 합니다. 여기서의 키 포인트는 DRY:Don't Repeat Yourself(스스로 반복하지 마세요)입니다. 여러분의 어플리케이션이 필요로 하며 여러분이 필요로 하는 모든 것을 계산하는 절대적이고 최소한의 상태를 알아내세요. 예를 들면, 만일 여러분이 TODO 리스트를 만든다면, TODO 아이템들의 배열을 갖고 있으세요. 그리고 count를 위한 별개의 상태 변수를 갖고 있지 마세요. 대신에, TODO count가 필요할 때는 그냥 TODO 아이템 배열의 length를 가져오시면 됩니다.

우리 예제 어플리케이션에서, 모든 데이터를 생각해보세요. 우리는 다음과 같은 데이터를 갖고 있습니다.

  • 제품의 원래 리스트
  • 유저가 입력한 검색 텍스트
  • 체크박스의 값
  • 제품의 필터링된 리스트

하나하나 훑어보고 어떤 것이 상태가 되어야 하는지 알아봅시다. 간단하게 각각의 데이터에 대해 3가지 질문을 던져봅시다.

  1. 부모의 props를 통해 온 데이터 인가요? 그렇다면, state가 아닙니다.
  2. 시간이 가도 변화하지 않는 데이터인가요? 그렇다면, state가 아닙니다.
  3. 여러분의 컴포넌트에서 다른 state나 props를 기반으로 계산할 수 있나요? 그렇다면, state가 아닙니다.

제품의 원래 리스트는 props로 넘어갑니다. 그러므로 state가 아닙니다. 검색 텍스트와 체크 박스는 state인 것처럼 보입니다. 왜냐하면 두 개는 시간이 지남에 따라 변화하고 다른 것들로부터 계산될 수 없으니까요. 그리고 마지막으로 필터링된 리스트는 state가 아닙니다. 왜냐하면 제품의 원래 리스트에서 검색 텍스트 값과 체크박스의 체크 여부로 계산되어 표기될 수 있기 때문입니다.

그래서 결과적으로, 우리의 state는

  • 사용자가 입력한 검색 텍스트
  • 체크박스의 값

입니다.

단계 4: State가 어디에 위치해야 하는지 구분하기

리액트처럼 생각하기: 단계 4를 코드펜에서 보세요.

이제 우리는 우리 앱 상태 중에 가장 최소한도로 필요한 것들이 어떤 것인지 알아냈습니다. 다음으로, 우리는 어떤 컴포넌트가 변화하는지 구분해야 합니다.

기억하세요: 리액트는 계층의 아래 방향으로만 흘러가는 데이터 흐름을 갖고 있습니다. 아직 어떤 컴포넌트가 어떤 state를 가져야 하는지는 분명하지 않습니다. 새로 리액트에 접근하는 사람들이 가장 이해하기 힘든 부분이 바로 이 부분입니다. 그러니 이것을 알아내기 위해 다음과 같은 절차를 따라봅시다.

여러분의 어플리케이션에서 각 state에 대해 :

  • 모든 컴포넌트가 그 state를 기반으로 무언가 렌더링하는지 확인하세요.
  • state를 공통으로 소유하는 컴포넌트가 있는지 찾아보세요. (계층에서 그 state를 필요로 하는 모든 컴포넌트의 상위에 있는 하나의 컴포넌트를 찾으세요.)
  • 공통으로 소유하는 컴포넌트나 계층에서 위쪽에 위치한 컴포넌트가 그 state를 소유하는 것이 좋습니다.
  • 이 state를 소유해야 하는 컴포넌트가 어떤 컴포넌트일지 찾을 수 없다면, state를 소유하기 위한 간단한 새로운 컴포넌트를 하나 만들고 공통 소유 컴포넌트의 위치에 그 컴포넌트를 추가하세요.

여러분의 어플리케이션에 대해 다음과 같은 전략을 살펴봅시다.

  • ProductTable은 state를 기반으로 제품 리스트를 필터링할 필요가 있습니다. 그리고 SearchBar는 search text와 checkbox가 체크되었는지에 대한 state를 보여줄 필요가 있습니다.
  • 공통 소유 컴포넌트는 FilterableProductTable입니다.
  • 필터 텍스트와 체크박스 체크 확인 값은 개념적으로는 FilterableProductTable에 있어도 괜찮습니다.

좋습니다. 그래서 우리는 우리 state를 FilterableProductTable에 두기로 했습니다. 먼저, 인스턴스 프로퍼티를 추가합시다. 여러분의 어플리케이션의 초기 state를 반영하기 위해 this.state = {filterText: '', inStockOnly: false}FilterableProductTableconstructor에 추가합시다. 그 후에, filterTextinStockOnlyProductTableSearchBar에 prop으로 넘깁시다. 마침내, ProductTable에서 열을 필터링하기 위해 props값을 사용합니다. 그리고 SearchBar에서 form field들의 값을 세팅합니다.

이제 여러분은 여러분의 App이 어떻게 동작할지 볼 수 있습니다. filterText"ball"을 입력하고 앱을 새로고침해보세요.데이터 테이블이 올바르게 업데이트 되는지 볼 수 있을 것입니다.

단계 5: 반대 데이터 플로우 추가하기

리액트처럼 생각하기: 단계5를 코드펜에서 확인해보세요.

지금까지, 아래 계층으로 흐르는 props와 state를 함수로서 정확히 렌더링하는 앱을 만들었습니다. 다른 방향으로 흐르는 데이터를 지원할 시간입니다 : 계층 깊숙히 있는 폼 컴포넌트들은 FilterableProductTable 안에 있는 state를 업데이트할 필요가 있습니다.

여러분의 프로그램이 어떻게 동작하는지 쉽게 이해시키기 위해 리액트는 이러한 데이터 흐름을 명시적으로 작성합니다. 하지만 이러한 작업은 전통적인 양방향 데이터 바인딩보다 약간 더 많은 타이핑을 요구합니다.

예제의 현재 버전에서 만일 여러분이 입력값을 타이핑하거나 박스를 체크하길 원한다면, 여러분은 리액트가 여러분의 입력값을 무시하는 것을 보게 될 것입니다. 이건 의도적인 동작입니다. 우리는 inputvalue prop을 FilterableProductTable로부터 넘어온 state와 동일하게 세팅하였습니다.

우리가 어떤 일이 일어나길 원하는지 생각해봅시다. 우리는 사용자가 언제 form을 변경하던지 사용자의 입력 값을 반영하여 state를 업데이트하길 바랍니다. 컴포넌트들은 그들 자신의 상태만 업데이트 하는 것이 바람직하기 때문에, FilterableProductTableSearchBar에게 언제든 state가 업데이트 될 때 작동할 콜백을 넘길 것입니다. 우리는 onChange 이벤트를 입력 값이 들어왔음을 알리기 위해 사용할 수 있습니다. FilterableProductTable에 의해 넘겨진 콜백은 setState()를 호출할 것이고 그 후에 앱이 업데이트 될 것입니다.

들리기엔 굉장히 복잡하게 들릴지라도, 그냥 몇줄의 코드일 뿐입니다. 그리고 앱을 통해 여러분의 데이터가 어떻게 흐르는지는 굉장히 명시적입니다.

이게 끝입니다.

이 글을 읽으면서, 리액트로 앱과 컴포넌트를 빌드하는 것에 대해서 어느정도 힌트를 얻으셨으면 합니다. 평소에 하던 것보다 조금 더 많은 타이핑을 해야할지라도, 여러분이 작성한 코드는 쓰여진 것보다 훨씬 쉽게 읽힌다는 것을 기억하세요. 그리고 모듈화되고 명시적인 코드는 훨씬 더 읽기 쉽습니다. 커다란 컴포넌트 라이브러리를 만들면서, 여러분은 아마 이러한 명시성과 모듈성에 대해 감사하게 될 것입니다. 그리고 코드 재사용성도요. 여러분의 코드는 점점 양이 줄어들기 시작할 것입니다.