2차 프로젝트에서 멘토님이 한 번 읽어 보라고 추천해 주신 공식 문서 ‘리액트로 사고하기’를 프로젝트가 끝나고 여유가 생겨 읽어 보았다. 이전에 리액트로 프로젝트를 했던 경험이 있었기 때문에 팀원들보다 빠르게 많은 작업을 할 수 있었다. 이는 프로젝트에 큰 도움이 되었지만, 공식 문서를 읽고 난 후 기능을 많이 구현하는 것이 중요한 것이 아니라, 얼마나 고민하고 생각하며 작업해야 하는지가 배우는 입장에서 훨씬 중요하다는 것을 깨달았다.
React로 사고하기는 React app을 체계적으로 설계하고 구현하는 5단계 방법론을 설명하는 글이다. 설명은 검색 가능한 상품 테이블을 만드는 실제 예시를 통해 진행된다. 1. UI를 컴포넌트 계층으로 쪼개기
컴포넌트를 구현할 때 설계를 어떻게 하느냐에 따라 코드의 스타일이 정해진다. 컴포넌트는 이상적으로 한 번에 한 가지 일만 해야 한다는 단일 책임 원칙을 항상 생각하며 설계를 해야 한다. 이는 컴포넌트를 나누는 명확한 기준이 된다. 위의 예시를 보면, 검색 박스와 재고가 있는 상품만 볼 수 있는 필터, 테이블 전체를 감싸고 있는 컨테이너, 테이블 헤더와 테이블 row로 컴포넌트를 나눌 것 같다.
검색 박스와 필터 부분은 사용자에게 입력받은 데이터로 상품 리스트를 제어하고, 공통으로 사용하고 있는 테이블 컨테이너의 name과 price를 아래 두 테이블이 같이 사용해야 하므로 컨테이너 컴포넌트에 포함 시키고, 공통으로 사용되는 테이블의 헤더 부분과 마지막으로 각 테이블의 row로 컴포넌트를 나누면 될 것 같다.
이 글에서는 서버로부터 전달받은 데이터가 잘 구조화되어 있다면, UI 컴포넌트 구조와 자연스럽게 대응이 된다고 한다. 이유는 UI 구조와 데이터 모델은 사실 같은 정보의 아키텍처를 가지기 때문이다.
깊게 생각해 보진 않았지만, 이번 프로젝트를 설계하면서 데이터를 먼저 파악하고 UI 설계를 하면서 자연스럽게 이해하고 있는 부분이라는 생각이 들었다.
2. React로 정적인 버전 구현하기
상호작용하는 기능을 제외하고 데이터 모델로부터 UI를 렌더링하는 것 부터 먼저 구현해야 한다. 이때는 State를 사용하지 않고 Props로만 구현을 하는 단계이며, 하향식 (top-down)또는 상향식 (bottom-up) 두 가지로 접근할 수 있다.
이 글에서 정적인 버전을 먼저 구현하는 것을 추천할까? 정적인 버전은 구현이 쉽고, 생각할 것이 상호작용 버전보다 적어 기계적으로 빠르게 구현이 가능하기 때문이라고 한다. 더 생각해 보자면 복잡한 로직(상호작용)이 포함되기 전에 빠르게 시각적인 피드백으로 컴포넌트가 잘 분리되었는지 구조 검증과 props 설계를 확인할 수 있는 장점이 있을 수 있다. 만약 복잡한 상호작용이 포함된 컴포넌트의 구조를 변경하려면 꽤나 많은 시간이 들 것 같다.
3. 최소한의 데이터만 이용해서 완벽하게 UI state 표현하기
React는 말 그대로 데이터에 반응하는 라이브러리이다. state는 리액트가 기억해야 하는, 변경할 수 있는 최소 집합이라고 표현했다. 실제로 리액트는 state의 변화를 감지하고 사용자와 상호작용할 수 있게 만들어 준다. state이외 데이터는 리액트가 변경을 감지하지 못하고 리렌더링을 유발하지 않아 사용자에게 새로운 UI를 보여주지 못한다.
이는 쉽게 상호작용할 수 있게 만들어 주지만 불필요한 리렌더링을 유발해 성능 저하의 원인이 되기도 한다. 그래서 이 글에서는 애플리케이션이 필요로 하는 최소한의 state를 파악하고 적용하는 것을 강조한다.
state는 판별할 수 있는 기준은 아래 3가지로 결정할 수 있다
•시간이 지나도 변하지 않는가? ← 변하지 않는 다면 state가 아니다.
•부모로부터 props로 전달받는가? ← 전달 받는 다면 state가 아니다.
•컴포넌트 안에서 다른 state나 props를 통해 계산을 해여하는가? ← 역시 state가 아니다.
그렇다면 우리는 예시에서 사용자가 입력한 검색어와 체크박스 값 이 두가지이 state에 적합하다고 알 수 있다.
4. State가 어디에 있어야 할 지 정하기
애플리케이션에서 필요한 state를 정했다면, 이제는 어느 컴포넌트에서 state를 관리할지 정해야 한다. 프로젝트를 하다 보면, 자식 컴포넌트에서 사용하던 state를 끌어올려 부모 컴포넌트에서 관리 해야 할 때도 있다. 고민 없이 state를 설계했다면 복잡한 기능을 구현한 후에 수정을 해야 해서 머리가 아팠던 적이 있었다.
리액트는 일반적으로 부모에서 형제로 props를 전달하는 단방향 데이터 흐름을 사용하고 있다. 형제 컴포넌트끼리 state를 공유할 수 없기 때문에 여러 컴포넌트에서 사용해야 할 state는 컴포넌트들의 가장 가까운 공통 부모에 두고 관리하는 것이 맞다.
예시에서도, 검색어 및 재고가 있는 상품 필터 state에 따라 사용자에게 보여줄 row 데이터가 달라진다. 따라서, row 데이터를 보여주는 테이블 컴포넌트와 검색 박스와 재고가 있는 상품만 볼 수 있는 필터 컴포넌트의 공통 부모 컴포넌트에 두 state를 두는 것이 적절하다고 생각한다.
5. 역 데이터 흐름 추가하기 (이벤트)
state는 리액트가 기억하고 변경할 수 있는 데이터이다. state 변경은 어디에서 해야 할까? 사용자 입력에 따라 state값을 변경하려면 반대방향의 데이터 흐름을 만들어야 한다.
왜 역 데이터 흐름일까? 공식 문서에서는 자세히 알려주지 않아서 고민해 보았다. 부모는 state 관리를, 자식은 state 변경에 따른 이벤트 처리와 UI처리를 맡는 책임 분리로 state의 변경이 부모에서 일어난다면 데이터 변화의 예측이 어려워 질 것 같다고 생각이 들었다. 디버깅할 때 state 상태 변경 헨들러가 해당 컴포넌트에 있다면 바로 추적이 가능하지만 부모에서 관리를 한다면, 다시 부모 컴포넌트를 디버깅해야 하기 때문에 데이터의 흐름을 쉽게 추적할 수 있다.
# 마치며
이 5단계 원칙을 차근차근 고민해보면서 많은 것을 꺠달았다. 솔직히 말하면 리액트로 프로젝트를 할때는 일단 동작하게 만들자는 생각이 컸다. 급하게 기능 구현하는 데만 집중하다 보니 정작 왜 이렇게 해야하는지에 대한 고민은 부족했던 것 같다. 멘토님의 추천으로 이 글을 읽고 이제는 리액트다운 사고 방식으로 접근할 수 있을 것 같다.
state를 사용할 때도 정말 필요한 state인지 고민하게 되고, 좀 더 명확한 컴포넌트 분리 기준도 생기게 되었다. 앞으로는 빠르게 기능을 만드는 것보다는 제대로 된 설계로 지속 가능한 코드를 작성하는 것에 더 집중하도록 노력할 것이다.