ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ReactJS] Thinking in React
    프로그래밍/웹 2018. 4. 5. 00:48

    React Thinking in React 홈페이지 원문

    Thinking in React

    자바스크립트를 사용하여 크고 빠른 앱을 만드는데에는 리액트가 최고의 툴이라고 생각한다. 페이스북과 인스타그램을 만드는데 사용하면서, 리액트의 확장성에 대한 증명은 완벽하게 되었다.

    리액트를 사용하는데 있어서 가장 훌륭한 부분중 하나는, 제작하려는 앱에대해서 고심하게 만든다는 것이다. 본 문서에서는 리액트를 사용하여 검색 가능한 상품 데이터 표를 만들면서, 어떠한 사고 흐름을 통해 구성되는지 알아 본다.

    Start With A Mock

    이미 JSON API와 디자이너로부터 전달받은 모조품이 있다고 가정하자. 모조품은 아래의 이미지와 같이 구성되어있다.

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

    Step 1: Break The UI Into A Component Hierarchy

    보통 모조품 디자인을 보고서 처음으로 하는 일은 각각의 요소들을 구분하기 위해 컴포넌트 별로 경계선 박스를 만들어 분리하고, 각 컴포넌트에 실제 코드에 사용할 이름을 붙이는 것이다. 디자이너를 협업을 하는 중이라면 이미 디자이너가 만든 포토샾 파일에 해당 작업들이 되어 있을 확률이 높다.

    그렇다면 어떤 요소가 리액트에서 하나의 컴포넌트를 차지하게 될까? 새로운 함수나 객체를 만들 때 처럼, 독립적인 일을 하는 요소를 하나의 컴포넌트로 구성하면 된다. 이를 single responsibility principle이라고 한다. 컴포넌트가 하는일이 점점 많아지게 되면, 작은 단위의 컴포넌트로 분리하여 구성해야 한다.

    컴포넌트를 명확하게 분리하였다면, JSON 데이터를 UI에 보여줄때, 보여주길 원하는 JSON 데이터와 컴포넌트가 정확하게 매핑 될것이다. 보통 데이터 모델을 컴포넌트로 분리하는 이유가 하나의 UI를 구성하기 위함이므로, 하나의 데이터를 보여주기 위한 컴포넌트들로 잘게 분류하여 구성하면 된다. 아래의 이미지는 UI에서 같은 일을 수행하는 요소들을 분리한 예제이다.

    분리된 컴포넌트 요소들을 살펴보면 다음과 같다.

    1. FilterableProductTable (orange): 주황색 박스 (UI 전체)로 둘러쌓인 부분은, 예제의 전체 요소를 포함한다.

    2. SearchBar (blue): 유저 입력을 받아들이는 역할을 하는 부분들은 파란색으로 표시하였다.

    3. ProductTable (green): 유저가 입력한 내용에 따라서 보여줄 데이터를 분류하여 표기할 요소이다.

    4. ProductCategoryRow (turquoise): 연한 청색 부분은 상품 분류 카테고리를 나타낸다.

    5. ProductRow (red): 빨간 부분은 실제 상품 정보를 나타낸다.

    ProductTable에서 NamePrice를 표기하는 부분은 따로 떼어내지 않고 ProductTable 컴포넌트에 포함하였다. 예제에서는 해당 헤더 부분이 단 한번만 존재하며, 각각이 따로 하는 일은 없이 내용만 보여주므로 하위 컴포넌트로 분리하지 않았다. 만일 해당 헤더 부분들을 통해 상품을 정렬하거나 화면에 변화를 주는 작업을 수행한다면, 새로운 컴포넌트로 분류하는것이 바람직하다.

    이미지에서 리액트 컴포넌트로 작성할 요소들을 분류하였으므로, 각각의 요소별 계층 구조를 분석하고 실제 코드를 작성해야한다. 계층 구조 분석은 자기 박스 내부에 있는 컴포넌트를 하위 요소로 분류하면 되므로 매우 쉬운 작업이다. 분류된 게층 구조는 아래와 같다.

    • FilterableProductTable

      • SearchBar

      • ProductTable

        • ProductCategoryRow

        • ProductRow

    Step 2: Build A Static Version in React

    컴포넌트 별 계층 구조까지 파악했으므로 실제로 앱을 만들어볼 시간이다. 가장 쉬운 방법은 사용자 입력은 받아 들이지 않고 단순히 모조품 이미지를 보여주는 UI를 먼저 구현해 보는 것이다. 그러나 단순 UI 구현을 하는 것은 타이핑 시간만 많이 들고 생각이 별로 필요없는 작업인데 반해, 유저 입력까지 고려하여 구현하는 것은 생각이 많이 필요한 작업이므로 무작정 정적인 코드를 하드코딩 해서는 안된다.

    향후 유저 입력까지 고려하여 정적인 앱을 만들기 위해서는, props 등을 활용한 재사용 가능한 컴포넌트를 만드는것이 중요하다. 지금 시점에서는 정적인 앱을 만들 것이므로 유저나 데이터와의 상호작용을 통한 UI 업데이트에 사용되는 state는 사용하지 않는 것이 편리하다. 정적인 앱을 만들때에는 props를 활용하는 것으로 충분하다.

    계층 구조가 명확 하므로 top-down 과 bottom-up 방식중 아무것이나 채택하여 구현하면 된다. 보통 간단한 앱의 경우에는 top-down 방식으로 구현하는 것이 편리하며, 대형 프로젝트의 경우에는 bottom-up 방식으로 구현하면서 구현되는 도중마다 하나의 요소를 위한 테스트 케이스를 구현하는 것이 편리하다.

    정적인 앱을 만드는 작업이 끝나면, 주어지는 데이터 모델을 이용해서만 화면을 렌더링 하는 재사용 가능한 컴포넌트 들이 구현되었을 것이다. 각각의 컴포넌트는 데이터의 변화에 상호작용 하지 않으므로 render() 메소드만을 가지고 있을 것이다. JSON 데이터 모델이 변경 되었을 경우에는 ReactDOM.render()를 다시 호출하면 변경된 데이터로 화면이 구성될 것이다.

    Step 2에서 작성되어야 하는 코드는 코드펜에서 확인 가능하다.

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

    데이터 변화와 UI가 상호작용하도록 만들기 위해서는 데이터 모델의 변화에 따라 트리거가 발생되도록 해야 한다. 이미 이전 문서에서 다뤘듯이 상태변화는 state를 통해 관리할 수 있다.

    앱을 정확하고 간결하게 구현하기 위해서는, 최소한으로 제한한 변경 가능한 state들이 무엇인지 생각해볼 필요가 있다. DRY: Don't Repeat Yourself를 적용하면 해결할 수 있다. 정말 최소한의 state를 추가하고 나머지 데이터들은 추가된 state를 통해 각종 연산으로 필요할때마다 도출해내는 것이 중요하다. ToDo 리스트 앱을 만든다고 가정하면, 단순히 todo 목록만 관리하면 굳이 todo 갯수를 state에 저장하지 않아도 도출해 낼 수 있는 것과 마찬가지 이다.

    다시 상품 목록 앱으로 돌아와서, 우리 앱에서는 어떤 데이터들이 변화에 영향을 받을지 알아보자.

    • 상품의 초기 리스트

    • 사용자가 입력할 검색 박스

    • 체크박스의 체크 여부

    • 사용자 입력에 따른 필터된 상품 리스트

    위의 4가지 요소들에 대해서 어떤것이 state로 저장되어야 할지 알아보자. 구분하기 어려울때는 아래의 3가지 질문을 스스로 답해보면 알 수 있다.

    1. 상위 요소로부터 props를 통해 전달 받는 데이터는 state로 저장할 필요가 없다.

    2. 시간이 지나도 절대 변화되지 않는 것들은 state로 저장할 필요가 없다.

    3. 다른 state로부터 각종 연산을 통해 도출 할 수 있는 데이터는 state로 저장할 필요가 없다.

    상품의 초기 리스트는 변화되지 않는 데이터이고 하위 컴포넌트로 props를 통해서 전달하므로 state에 저장할 필요가 없다. 사용자 입력 폼과 체크박스는 시간이 지남에 따라 사용자의 입력값에 맞추어 변화하므로 state에 저장할 필요가 있어보인다. 사용자 입력에 따른 필터링 된 상품 리스트는 언뜻 보면 데이터가 변화하는 것 같지만, 상품의 초기 리스트에서 사용자 입력 폼이나 체크박스에서 얻어낸 데이터를 통해 도출해 낼 수 있으므로, state로 저장하는 것은 적절하지 않다.

    최종적으로 아래의 2가지의 state가 필요한 것을 알수 있다.

    • 사용자가 입력한 검색 텍스트

    • 체크박스의 체크 여부

    Step 4: Identify Where Your State Should Live

    state로 저장할 데이터들을 정했으니, 그 state를 어느 컴포넌트에서 관리할지를 정해보자.

    리액트는 상위 컴포넌트로부터 하위 컴포넌트로만 데이터가 전달되는 단방향 데이터 플로우 형태를 지니고 있음을 명심하자. 이를 적용하여 state를 저장하는 컴포넌트를 정하는 것은, 리액트를 새로 접한 사용자들에게는 어려울 수 있으므로 아래의 몇가지 스텝을 통해서 판별해 보자.

    • state에 기반하여 UI를 렌더링 하는 컴포넌트를 찾는다.

    • 해당 컴포넌트들의 공통 상위 컴포넌트를 찾는다.

    • 찾아낸 공통 상위 컴포넌트에서 해당 state를 관리하도록 구현한다.

    • 공통 상위 컴포넌트가 존재하지 않을 경우, state를 관리하고 하위 컴포넌트로 전달만 하는 간단한 공통 상위 컴포넌트를 새로 구현한다.

    실제 예제 앱에 적용을 해보면 다음과 같다.

    • ProductTable 컴포넌트는 사용자 입력에 따른 state 변화에 반응하고, SearchBar 컴포넌트는 사용자가 입력한 데이터와 체크박스 체크여부를 렌더링 해야 한다.

    • 두 컴포넌트의 공통 상위 컴포넌트는 FilterableProductTable 이다.

    • 개념적으로 생각해봐도 FilterableProductTablestate를 관리하는 것이 적절하다.

    어느 컴포넌트에 state를 저장할지 정했으므로 실제로 구현해 보자. FilterableProductTable 컴포넌트의 constructor 메소드를 생성하고 해당 생성자에서 this.state = {filterText: '', inStockOnly: false};와 같은 초기화 코드를 통해 초기 state를 정의하자. 이후 ProductTableSearchBar 컴포넌트를 렌더링 할때 props의 일부로 this.state.filterTextthis.state.inStockOnly를 전달하면 된다.

    Step 4를 통해 작성되어야 하는 코드는 코드펜에서 확인할 수 있다.

    Step 5: Add Inverse Data Flow

    Step 4까지 진행하면서 상위 컴포넌트에서 하위 컴포넌트로 전달되는 props, state를 통해 하위 컴포넌트에서 state를 저장하지 않고 데이터를 렌더링 하는 앱을 구현하였다. 남은 것은 하위 컴포넌트인 SearchBox에서 렌더링하는 인풋 폼에서 받아들인 사용자 입력 데이터를 상위 컴포넌트인 FilterableProductTable 컴포넌트의 state에 반영하고 UI를 렌더링 하는 것이다.

    현재 버전에서는 아무리 인풋 데이터를 입력해도 UI에 변화가 없는 것을 확인할 수 있다. 이는 입력 폼의 데이터를 props를 통해 전달되는 state와 동기화는 시켜 놓았지만, 사용자가 데이터를 입력할때 입력 된 데이터를 state와 동기화 시키는 코드를 구현하지 않았기 때문이다.

    정확한 앱을 구현하기 위해서는 사용자가 데이터를 입력할때마다 변화된 데이터를 FilterableProductTable의 state에 반영해야 한다. 전체 state를 FilterableProductTable에서 관리하므로 콜백 함수를 SearchBar에 전달해서 state를 동기화 하는 방식을 사용해야 한다. 각종 인풋 폼에서 사용하는 onChange 이벤트 리스너 등록을 통해 이를 해결 할 수 있다. 콜백 함수 내에서는 setState() 메소드 호출을 통해 state를 변경시키고, 리액트는 이를 감지하여 필요한 엘리먼트를 다시 렌더링 한다.

    글로 쓰여진 해결책을 보면 복잡한 것 처럼 보이지만, 코드펜에서 확인 할 수 있는 최종 코드를 보면 Step 4의 코드에서 몇줄 변화가 안된 것을 볼 수 있다.

    And That's It

    본 문서가 리액트를 통해 앱을 작성할 때 생각하는 구조에 대해서 도움이 되었기를 바란다. 모듈화된 코드를 작성하는 것은 단순한 앱을 작성하는 것보다 조금더 많은 타이핑이 필요하지만, 작성된 이후에 코드를 재사용 하거나 코드를 분석하거나 코드를 고칠때 훨씬 용이하다. 특히나 대형 프로젝트를 리액트로 구현할 때, 이러한 모듈화된 코드 작성법이 매우 도움이 될 것이다. :)

    '프로그래밍 > ' 카테고리의 다른 글

    [React Advanced] Typechecking With PropTypes  (0) 2018.04.06
    [React Advanced] JSX In Depth  (0) 2018.04.05
    [ReactJS] Composition vs Inheritance  (0) 2018.04.03
    [ReactJS] Lifting State Up  (0) 2018.04.02
    [ReactJS] Forms  (0) 2018.03.30
Designed by Tistory.