반응형

React스럽게 생각하기

React는 규모가 크고 빠른 웹 어플리케이션을 자바스크립트로 만들 때 최고의 방법이라고 생각합니다. Facebook과 Instagram도 React를 사용하였으며 앱이 커질 때 아주 유용했습니다.

React의 여러 멋진 점 중 하나는 앱을 만들면서 앱에 대한 생각을 만든다는 것입니다. 이 문서에서는 React를 사용하여 검색가능한 상품 데이터 테이블을 만들 때 어떤 생각 프로세스를 통하는 지 살펴봅니다.

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

1단계: UI를 컴포넌트 계층으로 분리하기

가장 먼저 해야할 일은 모형의 각 컴포넌트 (와 서브컴포넌트)에 박스를 그리고 이름을 지어주는 것입니다. 만약 디자이너와 함께 일한다면 디자이너가 이미 이 작업을 끝냈을 수 있으니 한번 물어보세요! 디자이너의 포토샵 레이어 이름이 React 컴포넌트의 이름이 될 수도 있습니다.

그러나 어떤 것들을 컴포넌트로 만들어야할 지 어떻게 알 수 있을까요? 그냥 새 함수나 객체를 만들 지 말 지 결정하는 기준을 그대로 적용하세요. 그런 테크닉 중 하나는 단일 책임 원칙 (single responsibility principle) 이며, 이는 컴포넌트가 한가지의 작업만 하는 것이 이상적이라는 것입니다. 컴포넌트가 점점 커진다면 작은 서브컴포넌트들로 분리되어야합니다.

주로 JSON 데이터 모델을 유저에게 보여주게 되며, 만약 모델이 제대로 만들어져 있다면 UI (그리고 컴포넌트 구조도)는 제대로 매핑될 것입니다. 그 이유는 UI와 데이터 모델은 보통 인포메이션 아키텍쳐 (information architecture) 와 서로 깊게 연관되어있기 때문이며, 이 말은 UI를 컴포넌트로 세부화 시키는 것이 대부분 그렇게 대단한 일이 아니라는 것입니다. 각 컴포넌트가 데이터 모델의 한 조각을 나타내도록 분리하면 됩니다.

다섯개의 컴포넌트로 이루어진 간단한 앱을 하나 살펴봅시다. 각 컴포넌트가 표시하는 데이터를 이탤릭체로 표기했습니다.

  1. FilterableProductTable (orange): 예제 전체를 포함합니다
  2. SearchBar (blue): 모든 유저 입력 (user input) 을 받습니다
  3. ProductTable (green): 유저 입력 (user input) 을 기반으로 데이터 콜렉션 (data collection) 을 필터해서 보여줍니다.
  4. ProductCategoryRow (turquoise): 각 category 의 헤딩을 보여줍니다
  5. ProductRow (red): 각 product 행을 보여줍니다.

ProductTable 을 보면 “Name” 과 “Price” 레이블을 포함한 테이블 헤더는 해당 컴포넌트에 존재하지 않습니다. 이 건 선호의 문제이며 어느 쪽으로 선택할 지는 결정에 따릅니다. 이 예제에서는 ProductTable 이 ProductTable 의 책임인 데이터 콜렉션 렌더링의 일부이기 때문에 ProductTable 을 남겨두었습니다. 그러나 이 헤더가 복잡해지면 (즉 정렬을 위한 어포던스를 추가하는 등) 이 자체의 ProductTableHeader 컴포넌트를 만드는 것이 더 합리적일 것입니다.

이제 모형에서 컴포넌트를 확인하였으므로 이를 계층 구조로 나열해봅시다. 이 작업은 쉽습니다. 모형의 다른 컴포넌트에서 나타나는 컴포넌트는 계층 구조의 자식으로 나타냅니다.

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

2단계: React를 이용해 정적버전 만들기

CodePen에서 Thinking In React: Step 2를 살펴보세요.

이제 컴포넌트 계층구조가 만들어졌으니 앱을 실제로 구현해볼 시간입니다. 가장 쉬운 방법은 데이터 모델을 가지고 UI를 렌더링은 하지만 아무 동작도 하지 않는 버전을 만들어보는 것입니다. 이렇게 과정을 나누는 것이 좋은데 정적 버전을 만드는 것은 생각은 적게 필요하지만 타이핑은 많이 필요로 하고, 상호작용을 만드는 것은 생각은 많이 해야하지만 타이핑은 적게 필요로 하기 때문입니다. 나중에 더 살펴봅시다.

데이터 모델을 렌더하는 앱의 정적 버전을 만들기 위해 다른 컴포넌트를 재사용하는 컴포넌트를 만들고 props 를 이용해 데이터를 전달해줍니다. props 는 부모가 자식에게 데이터를 넘겨줄 떄 사용할 수 있는 방법입니다. 만약 state 에 대해 알고 있다면 정적 버전을 만들기 위해 state를 일절 사용하지 마세요 . state는 오직 상호작용을 위해, 즉 시간이 지남에 따라 데이터가 바뀌는 것에서 사용합니다. 앱의 정적 버전을 만들 때에는 필요없습니다.

앱을 만들 때 하향식 (top-down)이나 상향식 (bottom-up)으로 만들 수 있습니다. 다시 말해 계층 구조의 상층부에 있는 컴포넌트 (즉 FilterableProductTable 부터 시작합니다)부터 만들거나 하층부에 있는 컴포넌트 (ProductRow) 부터 만들 수도 있습니다. 간단한 예제에서는 보통 하향식으로 만드는 게 쉽지만 프로젝트가 커지면 상향식으로 만들고 테스트를 작성하면서 진행하는 것이 더 쉽습니다.

이 단계가 끝나면 데이터 렌더링을 위해 만들어진 재사용 가능한 컴포넌트의 라이브러리를 가지게 됩니다. 현재는 앱의 정적 버전이기 때문에 컴포넌트는 render() 메서드만 가지고 있을 것입니다. 계층구조의 최상단 컴포넌트 (FilterableProductTable)는 prop으로 데이터 모델을 받습니다. 데이터 모델을 변경하고 ReactDOM.render() 을 다시 호출하면 UI는 업데이트 됩니다. 어느 곳을 고쳐서 어떻게 UI가 업데이트되는 지 확인하는 일은 어렵지 않은데 지금은 크게 복잡한 부분이 없기 때문입니다. React의 단방향 데이터 플로우 (one-way data flow) (또는 단방향 바인딩 (one-way binding))는 앱을 모듈화 하기 좋고 빠르게 만들어줍니다.

이 단계를 실행하는 데 도움이 필요하다면 React 문서 를 참고하세요.

짧은 소개: Props vs State

React 에는 두가지 데이터 “모델”인 props와 state가 있습니다. 이 둘 사이의 차이점을 이해하는 것이 중요합니다. 만약 차이점이 제대로 기억나지 않는다면 공식 리액트 문서 를 살펴보세요.

3단계: UI state에 대한 최소한의 (하지만 완전한) 표현 찾아내기

UI를 상호작용하게 만드려면 기반 데이터 모델을 변경할 수 있는 방법이 있어야합니다. React는 state 로 이를 쉽게합니다.

앱을 올바르게 만들기 위해서는 앱에서 필요로 하는 변경가능한 state의 최소 집합을 생각해보아야 합니다. 여기서 핵심은 DRY: Don’t Repeat Yourself 원칙입니다. 어플리케이션이 필요로 하는 state를 완전하지만 작은 형태로 표현할 방법을 찾아내고 다른 모든 것은 필요할 때 state에서 계산하면 됩니다. 예를 들어 TODO 리스트를 만든다고 하면, TODO 아이템을 저장하는 배열만 유지하고 TODO 아이템의 갯수를 가지는 state를 별도로 가질 필요는 없습니다. 대신 TODO 갯수를 렌더링해야한다면 TODO 아이템 배열의 길이를 가져오면 됩니다.

예제 어플리케이션 내 데이터들을 생각해봅시다. 우리는 현재,

  • 제품의 원본 목록
  • 유저가 입력한 검색어
  • 체크박스의 값
  • 필터링된 제품 목록

각각 살펴보고 어떤 게 state가 되어야하는 지 살펴봅시다. 각 데이터에 대한 질문을 해볼 수 있습니다.

  1. 부모로부터 props를 통해 전달됩니까? 그러면 확실히 state가 아닙니다.
  2. 시간이 지나도 변하지 않나요? 그러면 확실히 state가 아닙니다
  3. 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가요? 그렇다면 state가 아닙니다.

제품의 원본 목록은 props를 통해 전달되므로 state가 아닙니다. 검색어와 체크박스는 state로 볼 수 있는데 시간이 지남에 따라 변하기도 하면서 다른 것들로부터 계산될 수 없기 때문입니다. 그리고 마지막으로 필터링된 목록은 state가 아닌데 제품의 원본 목록과 검색어, 체크박스의 값을 조합해서 계산해낼 수 있기 때문입니다.

결국 state는

  • 유저가 입력한 검색어
  • 체크박스의 값

만 남습니다.

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

CodePen에서 Thinking In React: Step 4를 살펴보세요.

좋습니다. 이제 앱에서 최소한으로 필요하는 state가 뭔지 찾아냈습니다. 다음으로는 어떤 컴포넌트가 state를 변경하거나 소유 할 지 찾아야합니다.

기억하세요: React는 항당 컴포넌트 계층구조를 따라 아래로 내려가는 단방향 데이터 흐름을 따릅니다. 어떤 컴포넌트가 어떤 상태를 가져야하는 지 바로 결정하기 어려울 수 있습니다. 많은 초보자들이 이 부분을 가장 이해하기 어려워합니다 아래 과정을 따라해보세요.

어플리케이션이 가지는 각각의 state에 대해서

  • state를 기반으로 렌더링하는 모든 컴포넌트를 찾으세요
  • 공통 오너 컴포넌트 (common owner component)를 찾으세요 (계층 구조 내에서 특정 state를 필요로하는 모든 컴포넌트들의 위에 있는 하나의 컴포넌트).
  • 공통 오너 혹은 더 상위에 있는 컴포넌트가 state를 가져야합니다.
  • state를 소유할 적절한 컴포넌트를 찾지 못하였다면, 단순히 state를 소유하는 컴포넌트를 하나 만들어서 공통 오너 컴포넌트의 상위 계층에 추가하세요.

이 전략을 어플리케이션에 적용해봅시다.

  • ProductTable 은 state에 의존한 상품 리스트의 필터링해야하고 SearchBar 는 검색어와 체크박스의 상태를 표시해주어야합니다.
  • 공통 오너 컴포넌트는 FilterableProductTable 입니다.
  • 의미상으로도 FilterableProductTable 이 검색어와 체크박스의 체크 여부를 가지는 것이 타당합니다.

좋습니다. state를 FilterableProductTable 에 두기로 했습니다. 먼저 인스턴스 속성인 this.state = {filterText: '', inStockOnly: false} 를 FilterableProductTable 의 constructor 에 추가하여 어플리케이션의 초기 상태를 반영합니다. 그리고 나서 filterText 와 inStockOnly 를 ProductTable 와 SearchBar 에 prop으로 전달합니다. 마지막으로 이 props를 사용하여 ProductTable 의 행을 정렬하고 SearchBar 의 폼 필드 값을 설정하세요.

이제 어플리케이션의 동작을 볼 수 있습니다. filterText 를 "ball" 로 설정하고 앱을 새로고침 해보세요. 데이터 테이블이 올바르게 업데이트 된 것을 볼 수 있습니다.

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

CodePen에서 Thinking In React: Step 5를 살펴보세요.

지금까지 우리는 계층 구조 아래로 흐르는 props와 state의 함수로서 앱을 만들었습니다. 이제 다른 방향의 데이터 흐름을 만들어볼 시간입니다. 계층 구조의 깊숙한 곳에 있는 폼 컴포넌트에서 FilterableProductTable 의 state를 업데이트할 수 있어야합니다.

React는 이러한 데이터 흐름을 명시적으로 보이게 만들어서 프로그램이 어떻게 동작하는 지 쉽게 파악할 수 있게 하지만 전통적인 양방향 데이터 바인딩 (two-way data binding)과 비교하면 더 많은 타이핑을 필요로 합니다.

현재 버전 예제에서 타이핑을 하거나 체크박스를 체크하려고 하면 React가 사용자의 입력을 무시합니다. 이는 의도한 것인데 FilterableProductTable 에서 넘어온 state가 항상 input의 value prop과 동일하기 떄문입니다.

우리가 어떤 걸 원하는 지 생각해봅시다. 우리는 사용자가 폼을 변경할 때마다 사용자의 입력을 반영할 수 있도록 state를 업데이트하기를 원합니다. 컴포넌트는 그 자신의 state만 변경할 수 있기 때문에 FilterableProductTable 는 SearchBar 에 콜백을 넘겨서 state가 업데이트되어야 할 때마다 호출되도록 할 것입니다. 우리는 input에 onChange 이벤트를 사용해서 알림을 받을 수 있습니다. FilterableProductTable 에서 전달된 콜백은 setState() 를 호출할 것이며, 앱은 업데이트될 것입니다.

복잡하게 들리지만 코드에선 몇줄밖에 안됩니다. 그리고 앱 전체적으로 데이터가 흐르는 모습을 명시적으로 볼 수 있습니다.

 

 

 


출처 :https://reactjs-kr.firebaseapp.com/docs/thinking-in-react.html

반응형
반응형

컴포넌트 분리

컴포넌트는 어떤 문맥이든 가장 작은 단위로서 언급됩니다.¹ 프로그램이나 모듈의 구성 단위로서도 그러합니다.² 그렇기 때문에 컴포넌트라고 하면 프로그램 언어 수준이 아닌 소프트웨어 디자인 수준에서 나눌 수 있는 가장 작은 단위를 의미합니다. 이 의미를 프론트엔드 라이브러리 또는 프레임워크에 적용하면, 웹 앱을 구성 하는 데 있어 가장 작은 단위가 됩니다. 실제로 우리가 많이 사용하는 React를 살펴보면 웹 앱을 구성하는 가장 작은 단위는 컴포넌트 입니다. 페이지를 만드는 것도 컴포넌트이고 아주 작은 요소를 만드는 것도 컴포넌트 입니다. 그렇기 때문에 우린 컴포넌트를 언제 그리고 어떻게 더 작은 단위로 나누어야 하는지 고민하게 됩니다. 그 과정에서 나온 Presentational Components와 Container Components는 가장 유명한 기준 중 하나이고 Atomic Design의 응용이 사용되고 있기도 합니다. 그럼에도 불구하고 컴포넌트를 언제 그리고 어떻게 나눠야 하는지 막막한 경우가 많습니다. 왜냐하면 종종 언급되곤 하는 방법들은 이해하기 쉽지만 적용하기가 어렵고 예외적인 상황이 많이 발생하기 때문입니다. 결국 적용하는 데 있어서 일관성이 깨지고 어느새 앱은 유지보수하기에 충분히 복잡해져 있습니다.

 

이 글은 컴포넌트의 분리라는 문제에 대해 현재 제가 갖고 있는 아이디어를 소개합니다. 따라서 시간이 지나보니 잘못 되었거나 더 개선될 수 있습니다. 그렇기 때문에 이 글은 기록 그리고 공유에 초점을 맞췄습니다.

전 프론트엔드 개발이 어렵다고 생각합니다. 마땅히 정해진 규칙은 없지만 갈수록 복잡성은 커져갑니다. 빠르게 변하는 기술이 프론트엔드 개발을 이해하는 데 어렵게 만든다는 건 이제 새삼스럽지도 않습니다. 프론트는 기획과 디자인, 백엔드와 인프라 등 모든 방법과 기술을 조합한 결과가 보여지는 곳입니다. HTML과 CSS, Javscript 그리고 React로 조금만 시간을 들이면 화면에 보이는 이쁘고 잘 동작하는 요소들을 만들던 프론트엔드는 갈수록 다루기 어려워지는 느낌입니다. 그렇기 때문에 잘 만들어야 합니다.

 

그럼 프론트엔드 개발을 잘 하기위한 방법 중 하나인 컴포넌트 분리에 대해 살펴보겠습니다.

언제 나눠야 할까?

컴포넌트를 만드는 기준, 즉 나누는 기준 중 가장 많이 선택되는 이유는 재사용성과 복잡성입니다.

코드가 재사용 될 가능성이 있다면 새로운 컴포넌트를 만드는 건 좋은 생각입니다. 코드가 복잡하다면 가독성을 개선하고 유지보수 할 수 있게 만들기 위해 컴포넌트를 분리할 수 있습니다.³

컴포넌트는 UI를 독립적이고 재사용 가능한 조각으로 나눌 수 있습니다. 그리고 각 조각을 독립적으로 고려할 수 있습니다.⁴

재사용 가능한 컴포넌트를 만들어 놓는 것은 더 큰 앱에서 작업할 때 두각을 나타냅니다. UI 일부가 여러 번 사용되거나 UI 일부가 자체적으로 복잡한 경우에는 별도의 컴포넌트로 만드는 게 좋습니다.⁵

컴포넌트를 만들 때 가장 많이 발생하는 실수 다섯가지⁶
1. 복잡한 컴포넌트를 만든다.
2. 하나의 컴포넌트에 여러 책임을 추가한다.
3. 몇몇 동작하는 부분을 결합하여 컴포넌트를 만든다.
4. 비지니스 로직을 컴포넌트에 추가한다.

아래에서는 이 내용들을 더 자세하게 살펴보고 분리하는 이유와 방법을 정리해보려고 합니다.

A. 재사용 가능한 컴포넌트

A-1. 재사용 가능하다는 것은 일반적이라는 것

재사용이 가능하다는 건 그만큼 일반적이라는 걸 의미합니다. 예를 들어보면, ‘탈것’ 이라는 건 ‘자동차’, ‘자전거’ 보다 일반적입니다. 이것은 포함 관계로 봤을 때 포괄적이라는 걸 의미합니다.

그리고 ‘탈것’은 조금 더 보편적인 속성을 갖습니다.

컴포넌트로 예를 들어보면, 두 개의 버튼 컴포넌트가 아래와 같이 있을 때

function Button(...) {
  return <button>...</button>
}

function ButtonWithIcon(...) {
   return (
     <div>
       <i>...</i>
       <button>...</button>
     </div>
   );
}

<button>…</button> 은 모든 버튼을 포함하는 포괄적인 개념입니다. 즉, <button>…</button>이라는 요소는 Button과 ButtonIcon을 모두 포함합니다. 그리고 <button>…</button>은 Button과 ButtonWithIcon 보다 보편적인 속성을 갖습니다. 그렇기 때문에 ‘버튼’이라는 기능을 사용해야 하는 곳이라면 <button>…</button>이 가장 일반적이기 때문에 어디에든 사용할 수 있고 재사용 가능합니다. 반면 ButtonWithIcon은 그렇지 않습니다.⁷

우리는 컴포넌트의 재사용성을 고려할 때 일반적이라는 두 가지 측면 중 ‘속성이 보편적’인지 고민합니다. 즉, ‘다른 컴포넌트가 가져가서 사용할 수 있도록 보편적인 속성을 갖고 있는가?’를 고려합니다. 마치 아래 그림에서 빨간색으로 칠해진 부분을 만들어야 합니다.

재사성을 높이려면 더 많은 컴포넌트를 만족시켜야 하고 더욱 일반적이어야 합니다

그렇다면 어떻게 재사용 가능한 컴포넌트를 만들 수 있을지 구체적으로 살펴보겠습니다.

A-2. HTML 요소 측면에서의 재사용성

컴포넌트를 만드는 데 너무 집중을 하다보면 지금 다루고 있는게 HTML을 포함하고 있다는 사실을 까먹을 때가 종종 있습니다. 그래서 컴포넌트를 분리할 때 HTML 요소 측면에서 고려해야할 것을 놓칠 때가 있습니다.

function ListComponent(...) {
  return (
    <ul>
      <li>
        <h3>...</h3>
        <p>...</p>
      </li>
      <li>
        <h3>...</h3>
        <p>...</p>
      </li>
    </ul>
  );
}

이런 형태의 컴포넌트가 있고 분리할 필요가 있다고 가정해보겠습니다. 분리한 컴포넌트는 어떻게 생겨야 할까요?

function ItemComponent(...) {
  return (
    <li>
      <h3>...</h3>
      <p>...</p>
    </li>
  );
}

// 또는

function SomethingComponent(...) {
  return (
    <>
      <h3>...</h3>
      <p>...</p>
    </>
  );
}

다양한 의견이 있을 수 있지만 개인적으로 SomethingComponent와 같이 분리해야 한다고 생각합니다. ItemComponent는 ul과 같은 리스트 요소에서 사용하도록 강제되는 측면이 있는 반면, SomethingComponent는 리스트 요소 말고 다른 곳에서도 사용할 수 있기 때문입니다. 즉, 조금 더 보편적인 속성을 가져야 하고(li, h3, p가 아닌 h3, p), 포괄적이어야(리스트 아이템이 아니라 다른 무언가) 합니다. (사실 h3도 사용할 수 있는 맥락이 한정적이기 때문에 재사용성을 떨어뜨리는 원인 중 하나 입니다.)

그래서 전 ListComponent를 만들 때 li 요소를 블록으로 간주하지 않는 방법을 선호합니다. 즉, li 요소 바로 아래에 구성하는 게 아니라 다른 적당한 요소를 사용합니다.

function ListComponent(...) {
  return (
    <ul>
      <li>
        <!-- 다른 요소를 블록으로 활용합니다. -->
        <section>
          <h3>...</h3>
          <p>...</p>
        </section>
      </li>
      ...
    </ul>
  );
}

그리고 요소의 구조를 잡을 때 BEM의 개념을 사용하곤 합니다. 왜냐하면 BEM의 Block은 독립적인 재사용성을 고려해야 하기 때문이고 이 개념은 컴포넌트를 분리할 때 기준으로서 도움이 됩니다.

따라서 컴포넌트를 분리할 때 HTML 요소들이 주변 문맥에 크게 의존적이지 않을수록 좋습니다.

A-3. 중복을 고려한 재사용성

우리가 컴포넌트의 재사용성에 대해 말할 땐 보통 중복을 떠올립니다. 두 곳 이상에서 중복된 무언가 존재하고 ‘추출’하는 과정을 거칩니다. 그리고 추출한 것을 재사용합니다. (추출이란 단어를 사용한 이유는 컴포넌트로 분리하는 과정이 이미 존재하는 컴포넌트에서 중복된 걸 골라내는, 즉 추출하는 과정과 비슷하기 때문입니다.)

function Page1() {
  return (
    <ul>
      <li>
        <section>
          <h3>...</h3>
          <p>가격...</p>
          <p>요약...</p>
        </section>
      </li>
    </ul>
  );
}

function Page2() {
  return (
    <ul>
      <li>
        <section>
          <h3>...</h3>
          <p>가격...</p>
        </section>
      </li>
    </ul>
  );
}

이렇게 두 페이지에서 비슷한 형태의 요소 구성을 사용한다면 아래와 같이 컴포넌트로 추출 합니다.

function Page1() {
  return (
    <ul>
      <li>
        <Card ... />
      </li>
    </ul>
  );
}

function Page2() {
  return (
    <ul>
      <li>
        <Card ... />
      </li>
    </ul>
  );
}

그리고 추출한 Card 컴포넌트는 아래와 같이 만들어집니다.

function Card(props) {
  return (
    <section>
      <h3>...</h3>
      <p>가격...</p>
      {props.showSummary && <p>요약...</p>}
    </section>
  );
}

이렇게 둘 이상의 컴포넌트에서 사용할 재사용 가능한 컴포넌트를 만들 때 가장 큰 특징 중 하나는 조건문 입니다. 완벽하게 같은 걸 사용하면 문제가 안 되지만 서로 다른 부분이 있다면 조건문이 들어가게 됩니다. 지금은 아주 간단한 컴포넌트라 문제가 잘 드러나지 않지만, 우리가 현실에서 마주하는 재사용 컴포넌트는 아래와 같이 점점 거대해지곤 합니다.

// 추출 후 1달
function Card(props) {
  const [a, setA] = useState(props.a ? props.foo : props.bar);
  const condition1 = props.a && !props.b;
  
  return (
    <section>
      <h3>...</h3>
      <p>가격...</p>
      <div>
        <button>{a ? 'fooValue' : 'barBalue'}</button>
      </div>
      {props.showSummary && <p>요약...</p>}
      {condition1 && <div>...</div>}
    </section>
  );
}

// 더 미래는 생략...

이렇게 거대해지는 건 상위 컴포넌트와 하위 컴포넌트가 강하게 결합했기 때문입니다. 처음엔 아래 그림처럼 약하게 결합되었던 컴포넌트들이

시간이 지나면서 아래 그림의 두꺼워진 빨간 선처럼 강하게 결합된 것입니다.

강한 결합은 약한 결합보다 변경이 더 어렵다는 걸 의미합니다. 예를 들어, 처음 만들었을 땐 Card 컴포넌트를 다른 컴포넌트로 쉽게 바꿀 수 있었습니다. 또는 속성명을 쉽게 바꿀 수 있었습니다. 이젠 다른 컴포넌트로 바꾸거나 Card 컴포넌트의 속성 이름, 내부 로직을 바꾸기도 어렵습니다.

그리고 다른 문제도 있습니다. 화살표는 의존성을 의미합니다. Page1 컴포넌트와 Page2 컴포넌트는 Card 컴포넌트에 의존적입니다. 의존적이라는 건 Card 컴포넌트를 수정하면 Page1과 Page2가 영향을 받기 때문에 혹시 문제가 발생하진 않는지 살펴봐야 한다는 걸 의미합니다.

만약 Page1과 Page2 뿐만 아니라 더 많은 컴포넌트가 Card 컴포넌트를 사용할수록 문제는 더 심각해집니다. 불행하게도 우린 모두 이런 문제에 익숙합니다. 이런 문제는 왜 발생했을까요? 그리고 어떤 문제를 일으킬까요?

가장 먼저 컴포넌트가 반환하는 요소의 중복을 추출해서 재사용해야 한다는 접근 방법이 문제의 발단일 수 있습니다. 중복 제거와 관련된 DRY 원칙⁸은 중복을 겉모습으로 판단하지 않습니다. 만약 모습이 같은 두 코드가 같은 이유로 수정된다면 그 코드는 중복입니다. 하지만 같은 모습의 코드라도 수정의 이유가 다르다면 두 코드는 중복이 아닙니다.

추출한 컴포넌트 내부에 사용하는 방법에 따라 조건문이 추가된다는 건, 사용하는 컴포넌트들이 서로 다른 수정의 이유를 갖는 다는 걸 의미합니다. 즉, 중복 제거와 재사용의 대상이 아닙니다. 따라서 처음에 조건문이 들어갈 때부터 산불의 작은 불씨가 시작된 것이었습니다.

물론 조건문이 없는 컴포넌트를 만들기는 너무나도 어렵습니다. 단, 조건문을 추가할 때 컴포넌트의 관계에 어떤 영향을 주는지 이해할 필요는 있습니다.

다음으로 의존성입니다. Page1과 Page2 컴포넌트는 Card 컴포넌트에 의존적입니다. 즉, 여러 컴포넌트의 집합체인 상위 컴포넌트가 재사용을 위한 하위 컴포넌트에 의존적입니다. 이렇게 되면 Page1과 Page2 컴포넌트와 같은 상위 컴포넌트들은 변경의 이유가 너무 많아집니다. 즉, Page1의 Card에 대해 수정 요구사항이 들어왔을 때 Card 컴포넌트만 수정하는 게 아니라 Page2 컴포넌트도 수정해야할 수 있습니다.

이 문제들을 해결하는 방법 중 하나는 재사용하려는 컴포넌트에는 정말 공통적인 것들만 남겨두고 사용하는 컴포넌트의 고유한 것은 속성으로 전달하는 것입니다.

function Page1() {
  return (
    <ul>
      <li>
        <Card
          summary={<p>요약...</p>}
        />
      </li>
    </ul>
  );
}

function Page2() {
  return (
    <ul>
      <li>
        <Card ... />
      </li>
    </ul>
  );
}

function Card(props) {
  return (
    <section>
      <h3>...</h3>
      <p>가격...</p>
      {props.summary}
    </section>
  );
}

이렇게 하면 Page1만의 특징인 summay는 Page1이 관리하고 Card는 이에 대해 신경 쓸 필요가 없습니다. 이 말은 Page1에서 summary로 전달하는 요소를 수정해도 Page2 컴포넌트를 살펴볼 필요가 없다는 것을 의미합니다. 물론 현실 세계의 컴포넌트는 이렇게 호락호락하지 않습니다. 요소의 위치나 스타일 등의 상호 의존성도 존재할 수 있기 때문입니다. 하지만 상태나 조건문 등의 결합이 사라진 것만으로도 효과를 볼 수 있고, 개발을 하면서 신경쓸 부분이 객관적으로 줄었다는 건 분명 긍정적입니다. 특히나 이 방법은 props drilling을 피하거나 컴포넌트의 제어를 역전하는 등 좋은 점을 더 많이 갖고 있고 공식문서에서도 소개하는 만큼 반드시 숙지하고 있을 필요가 있습니다. 또한 이렇게 속성으로 컴포넌트를 전달해야 하는 이유와 방법을 잘 안내하고 있는 문서 링크를 공유합니다.

React component as prop: the right way

말씀드렸던 것처럼 이 방법은 결합도와 의존성 문제를 해결할 수 있는 방법 중 하나 입니다. 그렇기 때문에 재사용을 위해 컴포넌트를 분리한다면 어떤 점을 주의해야 하는지 따져보고 그에 맞는 해결책을 상황에 맞게 찾아보는 게 정말 중요합니다.

B. 복잡한 컴포넌트

컴포넌트를 나누는 중요한 또 다른 이유 중 하나는 컴포넌트의 복잡성입니다. 컴포넌트가 복잡해지는 이유는 여러가지가 있습니다. 여기에선 두 가지 경우를 살펴보려고 합니다.

B-1. 컴포넌트가 여러 책임을 갖는 경우

여러 책임을 갖는 컴포넌트는 컴포넌트 분리 없이 만든 거대한 페이지 컴포넌트와 비슷합니다.

function Page(props) {
  // 선택한 탭을 변경하면 보여주는 내용을 변경합니다.
  // 페이징을 다룹니다.
  // 단어를 검색을 합니다.
  // 검색 조건 토글을 다룹니다.
  // 등등
}

이렇게 되면 기능 간에 결합이 강하게 발생해서 수정이 쉽지않습니다. 이건 마치 삼체문제와 비슷해서 서로 상호작용하는 기능이 많아지는 것보다 문제의 복잡성이 더 빨리 어려워진다는 것을 의미합니다.

그렇기 때문에 컴포넌트를 책임에 맞게 나눠서 문제를 단순화 해야 합니다. Page 컴포넌트가 탭, 검색, 페이징 그리고 이 정보들을 취합해 컨텐츠를 보여주는 등 모든 책임을 갖지 않도록 해야 합니다. 따라서 아래 그림처럼 컴포넌트를 분리해서 책임을 나눠야 합니다.

각 컴포넌트는 각자의 UI에 대한 책임(B)을 갖습니다. 그리고 컨텐츠 컴포넌트와의 소통은 A라는 경로를 통해서만 이뤄집니다. 탭 컴포넌트를 예로 들면

function Tab(...) {
  // B
  const [selectedTabName, setSelectedTabName] = useState('인기');

  const handleSelectTab = (tabName) => {
    // B
    setSelectedTabName(tabName);

    // A
    컨텐츠 컴포넌트와 소통수단(tabName); // 예를 들어, Context API
  };
  
  return (
    <div>
      <button
        type="button"
        style={{ selectedTabName === '인기'라면 하이라이트 }}
        onClick={() => handleSelectTab('인기')}
      >
        인기글
      </button>
      <button
        type="button"
        style={{ selectedTabName === '추천'이라면 하이라이트 }}
        onClick={() => handleSelectTab('추천')}
      >
        추천글
      </button>
    </div>
  );
}

와 같습니다. 그리고 분리를 통해 페이지 컴포넌트에 모두 모여있으면 발생할 수 있는 다른 컴포넌트와의 직접적인 상호작용(C)을 제거하거나 차단합니다. 이 방법은 페이지 컴포넌트가 아니라도 여러 컴포넌트에 적용될 수 있습니다. 이 방법은 캡슐화라고 할 수도 있습니다. 여러 책임이 한 곳에 모여있으면 각 컴포넌트가 서로 강하게 결합될 가능성이 높아집니다. 따라서 컴포넌트가 자신의 책임을 갖도록 분리하고 다른 컴포넌트와 상호작용 할 땐 정해진 방법으로 하는 게 좋습니다. 즉, 스스로와 관련된 변경은 각 컴포넌트가 책임짐으로써 감추고, 잘 변경되지 않는 외부와의 메시징은 제한하는 캡슐화는 컴포넌트를 관리하는 좋은 방법 중 하나입니다. 이런 경우 컴포넌트를 분리합니다.

B-2. 컴포넌트에 비지니스 로직이 있는 경우

이전에 적었던 글에서도 다뤘던 내용이지만, 일반적으로 유저 인터페이스(UI)와 비지니스 로직은 변경의 속도, 즉 빈도⁹가 다릅니다. 예를 들어, 상품 카드에 환불이 가능한지 표시하는 방법은 많고 수시로 바꿀 수 있습니다. 빨간색이었다가 회색이 될 수 있고 보여줬다가 안 보여줄 수도 있습니다. 그리고 이런 요청은 실제로 많이 발생합니다. 하지만 환불 가능한 조건을 변경하는 건 사업 초기에 한 번 정해지고 바뀌지 않을 수도 있습니다.

하지만 컴포넌트에 비지니스 로직이 포함되어있다면 빈번한 UI 변경에 따라 자주 영향을 받을 수 있습니다.

그리고 비지니스 로직은 현실 세계의 비지니스 규칙이기 때문에 영향을 자주 받고 변경에 노출되면 문제가 될 수 있습니다. 예를 들어, 컴포넌트 A는 환불 가능 여부에 따라 true 또는 false가 필요하고, 컴포넌트 B는 true 또는 false가 필요했다가 상품의 타입도 같이 필요해졌다면 비지니스 로직의 코드를 수정해야 합니다. 보통 비지니스 로직처럼 여러 군데에서 사용하는 로직은 일반적인 성질을 갖는 무언가(가장 대표적으로 함수)로 분리하기 때문에 이 이슈는 더 큰 문제가 됩니다. 어디에서 해당 로직이 사용되는지 조사하고 변경의 영향을 판단해야 하기 때문입니다. 로직을 사용하는 모든 컴포넌트를 고려하다보면 로직이 같은 함수를 이름만 바꿔서 또 만드는 등의 문제가 발생하기도 합니다.

function isRefundable(product) {
  // Business Logic
  return true or false;
}

// 이 함수를 아래와 같이 변경합니다.

function isRefundable(product) {
  // Business Logic
  return true or false;
}

function getRefundableWithType(product) {
  // Business Logic
  return {
    type: product type,
    isRefundable: true or false,
  };
}

// 이 함수를 또 아래와 같이 변경합니다.

function refundable(product) {
  // Business Logic
  return true or false;
}

function isRefundable(product) {
  return refundable(product);;
}

function getRefundableWithType(product) {
  return {
    type: refundable(product);,
    isRefundable: true or false,
  };
}

// 등등...

따라서 UI와 비지니스 로직을 적절하게 분리하는 건 소프트웨어를 오랫동안 유지보수 하는 데 있어서 아주 중요합니다. 이 내용은 이전에 작성했던 글을 참고하시거나 발표자료를 보시면 더 자세하게 확인할 수 있습니다. 이렇게 컴포넌트에 비지니스 로직이 있다면 로직을 분리하게 되고 그 과정에서 컴포넌트를 책임에 맞게 분리하는 과정이 포함될 수 있습니다.

C. 렌더링 퍼포먼스

재사용과 복잡성 이외에 컴포넌트를 분리하면 좋은 기준 중 하나는 렌더링 퍼포먼스 입니다. 하나의 컴포넌트 안에서 서로 영향을 주지 않는 상태가 여럿 있으면 발생하는 문제입니다.

function Page1() {
  const [카드 호버 상태, set카드 호버 상태] = useState(false);
  const [탭 호버 상태, set탭 호버 상태] = useState('none');

  return (
    ...
    <ul>탭</ul>
    ...
    <ul>카드</ul>
    ...
  );
}

이 코드에서 탭과 카드는 서로 영향을 주지 않습니다. 하지만 탭에 호버를 하면 카드들이 렌더링되고 카드에 호버를 하면 탭이 렌더링 됩니다. 그리고 카드들 같은 경우 각 카드도 보통은 호버 상태가 다른 카드의 호버 상태에 영향을 주지 않습니다. 따라서 아래와 같이 분리를 하면 좋습니다.

function Page1() {
  return (
    ...
    <Tab></Tab>
    ...
    <ul>
      ...
      <li><Card></Card><li>
      ...
    </ul>
    ...
  );
}

function Tab() {
  const [탭 호버 상태, set탭 호버 상태] = useState('none');
  
  return (
    <ul>탭</ul>
  );
}

function Card() {
  const [카드 호버 상태, set카드 호버 상태] = useState(false);

  return (
    <section>...</section>
  );
}

특히 input을 다루는 경우 상태 변경에 대한 업데이트가 다른 컴포넌트로 전파될 수 있기 때문에 주의해야 합니다.

마무리

지금까지 제가 리액트 컴포넌트를 분리하거나 다룰 때 사용하는 기준에 대해 살펴봤습니다. 당연하지만 이 기준들이 정답은 아닙니다. 프로젝트가 처한 상황, 개발 문화, 더 넓게는 회사의 상황이나 구성원들과의 커뮤니케이션 등 많은 문제가 연관되어 있을 수 있기 때문입니다. 소프트웨어를 만들고 유지보수 한다는 건 영문 타이핑 만큼 단순했지만 갈수록 더 복잡하고 고려할게 많아지는 것 같습니다. 이 방법들이 이 글을 읽으시는 분들의 환경에 맞게 고려되고 참고가 되면 좋겠습니다.

마지막으로 유연한 컴포넌트를 만드는 데 도움이 많이 됐던 글들을 소개합니다.

[1]: 로버트 C. 마틴, 클린 아키텍처, 인사이트, 100p

[2]: https://www.techtarget.com/whatis/definition/component

[3]: https://stackoverflow.com/questions/46192426/how-to-tell-when-to-create-a-new-component

[4]: https://vuejs.org/guide/essentials/component-basics.html

[5]: https://ko.reactjs.org/docs/components-and-props.html, https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children

[6]: https://blog.bitsrc.io/5-steps-to-build-react-components-like-a-pro-fb1f3af6ba17

[7]: 조영호, 오브젝트, 위키북스, 437p

[8]: 조영호, 오브젝트, 위키북스, 309p

[9]: 조영호, 오브젝트, 위키북스, 229p

 

 

 


출처 : https://medium.com/@junep/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EB%A5%BC-%EB%B6%84%EB%A6%AC%ED%95%98%EB%8A%94-%EA%B8%B0%EC%A4%80%EA%B3%BC-%EB%B0%A9%EB%B2%95-e7cf16bb157a

반응형
반응형

원글: https://itnext.io/decoupling-ui-and-logic-in-react-a-clean-code-approach-with-headless-components-82e46b5820c

 

 

 

프런트엔드 개발 영역에서는 용어와 패러다임이 때로는 이해하기 어려울 수 있으며 ‘헤드리스 UI’ 또는 ‘헤드리스 컴포넌트’도 이 범주에 속할 수 있습니다. 이러한 용어들이 무엇을 의미하는지 궁금해서 고개를 갸웃거리고, 혼자만 그런 것이 아닙니다. 사실, 혼란스러운 이름에도 불구하고 이러한 개념들은 복잡한 사용자 인터페이스 관리를 상당히 단순화할 수 있는 매력적인 전략입니다. 

헤드리스 컴포넌트는 난해해 보일 수 있지만, 그 진정한 힘은 유연성, 재사용 가능성, 그리고 코드베이스의 구성과 깔끔함을 향상시킬 수 있는 능력에 있습니다. 이 글에서는 이 패턴이 정확히 무엇인지, 왜 유용한지, 그리고 인터페이스 디자인에 대한 접근 방식을 어떻게 혁신할 수 있는지에 대해 탐구해 볼 것입니다.

설명을 위해, 먼저 헤드리스 컴포넌트를 간단하면서도 효과적으로 적용하는 방법, 즉 두 개의 유사한 컴포넌트에서 ‘useToggle’ 훅을 추출하여 코드 중복을 줄이는 방법을 살펴보겠습니다. 이 예시는 사소해 보일 수 있지만, 헤드리스 컴포넌트의 핵심 원칙을 이해하는 데 도움이 됩니다. 공통 패턴을 인식하고 이를 재사용 가능한 부분으로 추출함으로써, 코드베이스를 간소화하고 더 효율적인 개발 과정을 위한 기반을 마련할 수 있습니다.

하지만 이것은 빙산의 일각에 불과합니다! 더 깊게 파고들면, 우리는 이 원칙이 실제로 적용되는 더 복잡한 사례를 만나게 될 것입니다. 그것은 향상된 입력 컴포넌트를 생성하기 위한 강력한 라이브러리인 Downshift를 활용하는 것입니다.

이 글을 모두 읽고나서, 헤드리스 컴포넌트에 대한 이해뿐만 아니라, 이 강력한 패턴을 자신의 프로젝트에 통합할 수 있는 자신감도 얻을 수 있길 바랍니다. 이제, 헤드리스 컴포넌트에 대한 혼란을 뒤로하고 혁신적인 잠재력에 대해서 알아봅시다.

 

 

토글 컴포넌트

토글은 수많은 애플리케이션에서 필수적인 부분을 차지합니다. “이 기기에서 내 정보 기억”, “알림 활성화” 또는 늘 인기 있는 “다크 모드”와 같은 기능을 뒤에서 조용히 수행합니다.

(사진: 토글 컴포넌트)

 

 

 

 

React에서 이러한 토글을 만드는 것은 놀라울 정도로 간단한 과정입니다. 어떻게 구현할 수 있는지 살펴보겠습니다.

const ToggleButton = () => {
  const [isToggled, setIsToggled] = useState(false);

  const toggle = useCallback(() => {
    setIsToggled((prevState) => !prevState);
  }, []);

  return (
    <div className="toggleContainer">
      <p>Do not disturb</p>
      <button onClick={toggle} className={isToggled ? "on" : "off"}>
        {isToggled ? "ON" : "OFF"}
      </button>
    </div>
  );
};

 

useState 훅은 초기 값이 false인 상태 변수 isToggled를 설정합니다. useCallback으로 생성된 toggle 함수는 호출될 때마다 (버튼 클릭 시) isToggled 값을 true와 false 사이에서 전환합니다. 버튼의 모양과 텍스트(“ON” 또는 “OFF”)는 isToggled 상태를 동적으로 반영합니다.

 

이제 완전히 다른 컴포넌트인 ExpandableSection을 만들어야 한다고 가정해 보겠습니다. 이 컴포넌트는 섹션의 세부 정보를 표시하거나 숨깁니다. 제목 옆에 버튼이 있으며, 클릭하여 세부 정보를 펼치거나 접을 수 있습니다.

 

 

(사진: ExpandableSection 컴포넌트)

구현도 그리 어렵지 않습니다. 아래와 같이 쉽게 할 수 있습니다.

const ExpandableSection = ({ title, children }: ExpandableSectionType) => {
  const [isOpen, setIsOpen] = useState(false);

  const toggleOpen = useCallback(() => {
    setIsOpen((prevState) => !prevState);
  }, []);

  return (
    <div>
      <h2 onClick={toggleOpen}>{title}</h2>
      {isOpen && <div>{children}</div>}
    </div>
  );
};

 

두 예제 사이에는 명백한 유사성이 있습니다. ToggleButton의 ‘on’ 과 ‘off’ 상태는 ExpandableSection의 ‘펼치기(expand)’ 와 ‘접기(collapse)’ 작업과 유사합니다. 이러한 공통점을 인식하면, 이 공통 기능을 별도의 기능으로 추상화할 수 있습니다. React 생태계에서는 사용자 정의 훅을 생성하여 이를 수행합니다.

 

const useToggle = (init = false) => {
  const [state, setState] = useState(init);

  const toggle = useCallback(() => {
    setState((prevState) => !prevState);
  }, []);

  return [state, toggle];
};

 

리팩터링은 상당히 간단해 보일 수 있지만, 표현(UI)에서 동작을 분리한다는 중요한 개념을 강조합니다. 이 시나리오에서, 사용자 정의 훅은 JSX로부터 독립된 상태 머신 역할을 합니다. ToggleButton과 ExpandableSection 모두 이 동일한 기본 로직을 활용합니다.

중간 규모의 프론트엔드 프로젝트에 상당한 시간을 투자해 본 사람들은, 대부분의 업데이트나 버그가 UI 시각적 요소와 관련된 것이 아니라 UI 상태 관리와 관련된 로직에 문제가 있다는 것을 알게 될 것입니다. 훅은 이러한 로직을 중앙 집중화하는 강력한 도구를 제공하여, 코드 분석, 최적화 그리고 유지보수를 더욱 쉽게 만듭니다.

헤드리스 컴포넌트

실제로 이 패턴을 사용하여 동작(또는 상태 관리)과 표현을 분리하는 훌륭한 라이브러리가 이미 많이 있습니다. 그리고 이러한 컴포넌트 라이브러리 중 가장 유명한 것은 Downshift 일 것입니다.

Downshift는 UI를 렌더링하지 않고 동작과 상태를 관리하는 헤드리스 컴포넌트의 개념을 적용합니다. render 속성 함수에서 상태와 일련의 액션을 제공하여 UI에 연결할 수 있게 합니다. 이러한 방식으로, Downshift는 복잡한 상태와 접근성 관리를 담당하는 동시에 UI 제어를 가능하게 해줍니다.

예를 들어, 드롭다운 목록을 만들고 싶다면, 당연히 목록의 데이터, 트리거, 그리고 선택한 항목을 강조하는 방법, 렌더링할 라인 수에 대한 몇 가지 사용자 정의가 필요합니다. 하지만 크로스 브라우저 및 크로스 디바이스를 포함하여 고려해야 할 수많은 예외 사항이 있기 때문에 접근성을 처음부터 구축하고 싶지 않습니다.

(사진: StateSelect 컴포넌트)

 

 

 

 

Downshift를 사용하면 몇 줄의 JSX만으로도 접근성 있는 select를 쉽게 만들 수 있습니다.

const StateSelect = () => {
  const {
    isOpen,
    selectedItem,
    getToggleButtonProps,
    getLabelProps,
    getMenuProps,
    highlightedIndex,
    getItemProps,
  } = useSelect({items: states});

  return (
    <div>
      <label {...getLabelProps()}>Issued State:</label>
      <div {...getToggleButtonProps()} className="trigger" >
        {selectedItem ?? 'Select a state'}
      </div>
      <ul {...getMenuProps()} className="menu">
        {isOpen &&
          states.map((item, index) => (
            <li
              style={
                highlightedIndex === index ? {backgroundColor: '#bde4ff'} : {}
              }
              key={`${item}${index}`}
              {...getItemProps({item, index})}
            >
              {item}
            </li>
          ))}
      </ul>
    </div>
  )
}

 

이 컴포넌트는 Downshift의 useSelect 훅을 사용하는 상태 선택자입니다. 이를 통해 사용자는 드롭다운 메뉴에서 상태를 선택할 수 있습니다.

 

  • useSelect 는 선택 입력에 대한 상태와 상호 작용을 관리합니다.
  • isOpen, selectedItem, highlightedIndex 는 useSelect 에 의해 제어되는 상태 변수입니다.
  • getToggleButtonProps, getLabelProps, getMenuProps, getItemProps 는 해당 요소에 필요한 속성을 제공하는 함수입니다.
  • isOpen 은 드롭다운이 열려 있는지 여부를 결정합니다.
  • selectedItem 은 현재 선택된 상태의 값을 보유합니다.
  • highlightedIndex 는 현재 강조 표시된 목록 항목을 나타냅니다.
  • 드롭다운이 열려 있으면, states.map은 선택 가능한 상태의 정렬되지 않은 목록을 생성합니다.
  • 스프레드 (...) 연산자는 Downshift의 훅에서 컴포넌트에 props를 전달하는 데 사용됩니다. 여기에는 클릭 핸들러, 키보드 탐색 및 ARIA 속성과 같은 것들이 포함됩니다.
  • 상태가 선택되면 버튼 내용으로 표시됩니다. 그렇지 않으면 ‘Select a state’라고 표시됩니다.
  •  

이 접근 방식은 렌더링에 대한 완전한 제어를 제공하므로, 애플리케이션의 모양과 느낌에 맞게 컴포넌트를 스타일링하고 필요한 경우 사용자 정의 동작을 적용할 수 있습니다. 또한 다양한 컴포넌트나 프로젝트 간에 동작 로직을 공유하는 데도 매우 좋습니다.

이미 이 패턴을 따르고 있는 헤드리스 컴포넌트 라이브러리도 몇 가지 더 있습니다.

  • Reakit: 접근 가능한 고급 UI 라이브러리, 툴킷, 디자인 시스템 등을 구축하기 위한 헤드리스 컴포넌트 세트를 제공합니다.
  • React Table: 조립하기 위한 헤드리스 유틸리티입니다. 훅 기반이며 모든 종류의 테이블을 만들 수 있습니다.
  • react-use: 여러 헤드리스 컴포넌트가 포함된 훅의 모음입니다.

좀 더 깊이 파보기

의도적으로 UI에서 로직을 계속 분리해 나가면, 점차적으로 계층 구조가 형성됩니다. 이 구조는 전체 애플리케이션에 걸쳐있는 기존의 계층형 아키텍처가 아니라 애플리케이션의 UI 일부에 한정된 구조입니다.

(사진: 헤드리스 UI 패턴)

 

 

 

이 배치에서 JSX(또는 대부분의 태그)는 최상위 계층에서 정의되며, 이 계층은 전달된 속성을 표시하는 것만을 담당합니다. 바로 아래에는 ‘헤드리스 컴포넌트’라고 불리는 것이 있습니다. 이 컴포넌트는 모든 동작을 유지하고 상태를 관리하며 JSX와 상호 작용할 인터페이스를 제공합니다. 이 구조의 기반에는 도메인별 로직을 캡슐화하는 데이터 모델이 있습니다. 이러한 모델들은 UI 또는 상태와 관련이 없습니다. 대신, 데이터 관리와 비즈니스 로직에 중점을 둡니다. 이 계층적 접근법은 문제를 깔끔하게 분리하여 코드의 명확성과 유지 관리성을 향상시킵니다.

균형 잡힌 시각

다른 유형의 기술과 마찬가지로 헤드리스 UI도 채택하기 전에 알아야 할 장단점이 있습니다. 먼저 헤드리스 UI의 이점에 대해 논의해 보겠습니다.

  1. 재사용성:헤드리스 컴포넌트의 주요 장점은 재사용성입니다. 로직을 독립적인 컴포넌트로 캡슐화함으로써, 여러 UI 요소에서 이러한 컴포넌트를 재사용할 수 있습니다. 이는 코드 중복을 줄일 뿐만 아니라 애플리케이션 전반에 걸쳐 일관성을 강화합니다.
  2. 관심사 분리: 헤드리스 컴포넌트는 로직과 표현을 명확하게 분리합니다. 이로 인해 코드베이스가 더 관리하기 쉽고 이해하기 쉬워지며, 특히 업무가 분산된 대규모 팀에게 유용합니다.
  3. 유연성: 헤드리스 컴포넌트는 프레젠테이션에 구애받지 않기 때문에 디자인 유연성을 높일 수 있습니다. 기본 로직에 영향을 주지 않고 원하는 만큼 UI를 사용자 정의할 수 있습니다.
  4. 테스트 가능성: 프레젠테이션과 로직이 분리되어 있기 때문에, 비즈니스 로직에 대한 단위 테스트 작성이 더 쉽습니다.

반면, 일체형 컴포넌트보다 조금 더 복잡하기 때문에 다음 고려 사항을 염두에 두고 현명하게 사용해야 합니다.

  1. 초기 부담: 더 단순한 애플리케이션 또는 컴포넌트의 경우, 헤드리스 컴포넌트를 생성하는 것이 과도한 엔지니어링처럼 느껴져 불필요한 복잡성을 초래할 수 있습니다.
  2. 학습 곡선: 이 개념에 익숙하지 않은 개발자들은 처음에 이해하기 어려울 수 있으며, 학습 곡선이 더 가파르게 느껴질 수 있습니다.
  3. 남용 가능성: 모든 컴포넌트를 헤드리스로 만들려고 하다 보면, 불필요한 경우에도 과도하게 사용하게 되어 코드베이스가 지나치게 복잡해질 수 있습니다.
  4. 잠재적 성능 문제: 일반적으로 큰 문제는 아니지만, 신중하게 처리하지 않으면 공통 로직을 사용하여 여러 컴포넌트를 다시 렌더링하는 것이 성능 문제로 이어질 수 있습니다.

기억하세요, 헤드리스 UI는 다른 아키텍처 패턴처럼 모든 것에 적용되는 만능 해결책이 아닙니다. 이를 사용할지 여부는 프로젝트의 특정 요구 사항과 복잡성을 기반으로 결정되어야 합니다.

요약

이 글에서, 복잡한 UI 작업을 처리하는 강력한 접근법인 헤드리스 UI의 세계를 탐구했습니다. 렌더링을 분리하는 것이 어떻게 더 유지보수가 쉽고 재사용 가능한 코드를 생성할 수 있게 해주며, 중복성과 잠재적인 버그를 줄일 수 있는지 살펴보았습니다. 먼저 useToggle이라는 사용자 정의 리액트 훅을 생성하고 두 개의 별도의 컴포넌트에서의 적용을 보여주는 간단한 예를 통해 이를 설명했습니다. 그런 다음 향상된 입력 컴포넌트 구현을 용이하게 하는 뛰어난 라이브러리인 Downshift와 함께 이 개념을 보다 복잡한 시나리오로 확장했습니다. ‘헤드리스’ 접근법에 대한 더 깊은 이해를 통해, 향후 프로젝트에서 더 확장 가능하고 유지보수가 쉬운 UI를 만드는데 이 패턴을 활용할 수 있기를 바랍니다.

반응형

'React' 카테고리의 다른 글

React 생각  (0) 2023.12.04
프론트엔드 아키텍처: 컴포넌트를 분리하는 기준과 방법  (1) 2023.12.04
useLayoutEffect  (0) 2023.11.27
React Hooks 소개  (2) 2023.11.27
Create React App(CRA)로 시작하기  (2) 2023.11.23
반응형

useEffect 와 useLayoutEffect의 차이

  1. useEffect는 asynchronous로 즉 비동기적으로 실행되는 반면
    useLayoutEffect  synchronous로 동기적으로 실행됩니다.
  2. 렌더 순서의 차이




    useEffect는 component가 화면에 paint된 이후 실행된다.



useLayoutEffect는 dom 이 페인트되기 직전 실행됩니다, 즉 레이아웃이 완성 되고 실행시켜야 할때 자주 사용됩니다.

퍼포먼스적으로 useEffect 주로 사용하나,기능적으로 꼭 필요 할때만 useLayoutEffect를 사용한다.

useEffect, useLayoutEffect 사용예

useEffect와 useLayoutEffect를 가장 잘 보여주는 예시로 확인해보자

import React, {useState, useEffect, useLayoutEffect, useRef} from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  const [show , setShow] = useState(false);
  const popup = useRef<HTMLDivElement>(null);
  const button = useRef<HTMLButtonElement>(null);

  useEffect(()=>{
    if(popup.current == null || button.current == null) return
    const { bottom } = button.current.getBoundingClientRect();
    popup.current.style.top = `${bottom + 25}px`
  },[show])

  return (
   <>
    <button ref={button} onClick={()=> setShow(prev => !prev)}>
      Click Here
    </button>
    {show && (
      <div style={{position: 'absolute'}} ref={popup}>
        This is popup
      </div>
    )}
   </>
  );
}

export default App;

useEffect 실행전 이미 페인팅이 된 요소가 useEffect 실행 이후 위치가 변하는것을 확인 할 수 있다.

import React, {useState, useEffect, useLayoutEffect, useRef} from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  const [show , setShow] = useState(false);
  const popup = useRef<HTMLDivElement>(null);
  const button = useRef<HTMLButtonElement>(null);

  useLayoutEffect(()=>{
    if(popup.current == null || button.current == null) return
    const { bottom } = button.current.getBoundingClientRect();
    popup.current.style.top = `${bottom + 25}px`
  },[show])

  return (
   <>
    <button ref={button} onClick={()=> setShow(prev => !prev)}>
      Click Here
    </button>
    {show && (
      <div style={{position: 'absolute'}} ref={popup}>
        This is popup
      </div>
    )}
   </>
  );
}

export default App;

useLayoutEffect 실행 이후 페인팅 되기 때문에 useEffect와 다르게 초기위치없이 바로 bottom + 25 px 위치에 페인팅 되는 것을 확인 할 수 있다.

 

 

reference

https://pubudu2013101.medium.com/what-is-the-real-difference-between-react-useeffect-and-uselayouteffect-51723096dc19
https://www.youtube.com/watch?v=wU57kvYOxT4

반응형
반응형

hooks를 사용하면 컴포넌트의 로직을 구성하여 클래스를 작성하지 않고도 컴포넌트를 작고 재사용 가능하게 만들 수 있습니다.

어떤 의미에서 이는 함수에 의존하는 React의 방식입니다

 

 

왜 hooks가 나타나게 되었냐면 이전 방식으로 작업하게되면 컴포넌트에서 랜더링을 해야하고  DevTools를 탐색하고 디버그하기가 어려운 형식의 div가 가득찬 DOM이 되기 때문입니다.

물론 단순히 null을 반환하거나 자체 DOM노드 없이 다른구성요소에 위임을할수 있습니다.

 

 

hooks를 사용하게되면 구성요소의 하향식 흐름에 의존하거나 고차구성요소와 같이 다양한 방식으로 추상화하는대신 구성요소 내부에서 흐름을 호출하고 관리할수 있게 됩니다.

 

후크는 구성 요소 사이 가 아닌 구성 요소 내부에 React 철학(명시적인 데이터 흐름 및 구성)을 적용합니다 . 이것이 바로 Hook이 React 컴포넌트 모델에 자연스럽게 적합하다고 생각하는 이유입니다.
렌더 소품이나 고차 구성 요소와 같은 패턴과 달리 Hooks는 구성 요소 트리에 불필요한 중첩을 도입하지 않습니다. 또한 믹스인의 단점도 겪지 않습니다.

 

 


 

 

useState()시작할 때 주목해야 할 중요한 점은 현재 사용할 수 있는 후크가 9개 있다는 것입니다.

 

간단하게 3가지의 hooks 알아보겠습니다.

 

 

 

useState()

어떤 수준에서든 React를 사용해본 적이 있다면 상태가 일반적으로 정의되는 방식에 익숙할 것입니다.

클래스를 작성하고 this.state클래스를 초기화하는 데 사용합니다.

 

Class 

class SomeComponent extends React.component {
  constructor(props)
  super(props);
  this.state = {
    name: Test Value
  }
}

 

Hooks

import { useState } from 'react';
    
function SomeComponent() {
  const [name, setName] = useState('Test Value');
  
  return
    <div>
      <p>Hello, {name}</p>
    </div>
}

ㅇㄴㅁ

ㅇㅁ

 

 

useEffect()

useEffect()따라서 상태를 정의하고 설정하는 것은 모두 훌륭하고 멋지지만, 짐작하셨겠지만 클래스나 중복 코드를 사용할 필요 없이 구성 요소에서 직접 효과를 정의하고 재사용하는 데 사용할 수 있는 또 다른 후크가 있습니다 . 

 

메소드의 각 라이프사이클(예 componentDidMount: componentDidUpdate, 및 componentWillUnmount).

 

 API 호출, DOM 업데이트, 이벤트 리스너 등을 언급합니다. 

 

React 문서에서는 이 후크에 대한 가능한 사용 사0례로 데이터 가져오기, 구독 설정 및 DOM 변경과 같은 예를 인용합니다. 장 큰 차이점은 렌더링 후에 실행된다는 useState()것입니다 useEffect(). 통과한 함수를 유지하고 렌더링이 발생한 후 DOM을 조정하고 그 이후의 모든 업데이트를 수행 하도록 React에 지시

 

더보기

const { useState, useEffect } = React

const App = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    axios("https://randomuser.me/api/?results=10")
      .then(response =>
        response.data.results.map(user => ({
          name: `{user.name.first} ${user.name.last}`,
          username: `{user.login.username}`,
          email: `{user.email}`,
          image: `{user.picture.thumbnail}`
        }))
      )
      .then(data => {
        setUsers(data);
      });
  }, []);
  
  return (
    <div className="users">
      {users.map(user => (
        <div key={user.username} className="users__user">
          <img src={user.image} className="users__avatar" />
          <div className="users__meta">
            <h1>{user.name}</h1>
            <p>{user.email}</p>
          </div>
        </div>
      ))}
    </div>
  )
}

효과를 연결하고 조건에 따라 트리거하는 것과 같이 훨씬 더 많은 기능을 수행할 수 있다는 점은 주목할 가치가 있습니다 .또한 메모리 누수를 방지하기 위해 외부 리소스 구독과 같이 효과가 실행된 후 정리해야 하는 경우도 있습니다.

리엑트 사이트 정리효과

 

 

 

Context and useContext()

React의 컨텍스트를 사용하면 상위 구성 요소에서 하위 구성 요소로 props를 전달할 수 있습니다. 

이를 통해 프롭 드릴링의 번거로움을 덜 수 있습니다. 

 

const CountContext = React.createContext();

 

App 구성 요소에서 카운터의 개수 상태와 증가/감소 메서드를 선언하고 구성 요소를 보관할 래퍼를 설정합니다. 

실제 카운터 구성 요소에 사용할 컨텍스트 후크를 추가하겠습니다.

const App = () => {
  //'useState()'를 사용하여 카운트 변수와 해당 상태를 정의
  const [count, setCount] = useState(0);
  
  // 클릭할 때마다 현재 `setCount` 변수 상태를 1씩 증가시키는 메서드를 구성
  const increase = () => {
    setCount(count + 1);
  };
  
  // 클릭할 때마다 현재 `setCount` 변수 상태를 1씩 줄이는 메서드를 구성
  const decrease = () => {
    setCount(count - 1);
  };

  //컨텍스트 값을 제공할 공급자가 포함된 카운터 구성 요소에 대한 래퍼
  return (
    <div>
      <CountContext.Provider
        value={{ count, increase, decrease }}
      >//값은 카운트 증가 또는 감소 메소드가 트리거될 때 업데이트됩니다.
        <Counter />// 다음에 생성할 Counter 구성 요소를 호출
      </CountContext.Provider>
    </div>
  );
};

 

 

useContext()객체를 받아들이고( CountContext제공자에 전달) React에게 우리가 원하는 값(`count)과 업데이트된 값을 트리거하는 메소드( increase및 decrease)를 정확하게 알려줄 수 있습니다. 

 

그런 다음에는 앱에서 호출하는 구성 요소를 렌더링하여 작업을 마무리합니다.

const Counter = () => {
  const { count, increase, decrease } = useContext(CountContext);
  return (
    <div className="counter">
      <button onClick={decrease}>-</button>
      <span className="count">{count}</span>
      <button onClick={increase}>+</button>
    </div>
  );
};

 

See the Pen React hooks - useContext by Kingsley Silas Chijioke (@kinsomicrote) on CodePen

 

 

 

 

훅설명

userReducer() . useState_ 유형의 감속기를 허용 (state, action) => newState하고 메소드와 쌍을 이루는 현재 상태를 반환합니다 dispatch.
useCallback() 메모된 콜백을 반환합니다. 인라인 콜백과 입력 배열을 전달합니다. useCallback입력 중 하나가 변경된 경우에만 변경되는 메모된 버전의 콜백을 반환합니다.
useMemo() 메모된 값을 반환합니다. "생성" 함수와 입력 배열을 전달합니다. useMemo입력 중 하나가 변경된 경우에만 기억된 값을 다시 계산합니다.
useRef() useRef.current전달된 인수( )로 속성이 초기화되는 가변 참조 객체를 반환합니다 initialValue. 반환된 개체는 구성 요소의 전체 수명 동안 유지됩니다.
useImperativeMethods useImperativeMethods를 사용할 때 상위 구성 요소에 노출되는 인스턴스 값을 사용자 정의합니다 ref. 언제나 그렇듯, ref를 사용하는 명령형 코드는 대부분의 경우 피해야 합니다. useImperativeMethods와 함께 사용해야 합니다 forwardRef.
useLayoutEffect 서명은 와 동일 useEffect하지만 모든 DOM 변형 후에 동기적으로 실행됩니다. DOM에서 레이아웃을 읽고 동기적으로 다시 렌더링하려면 이를 사용합니다. 내부에 예약된 업데이트는 useLayoutEffect브라우저가 페인트할 기회를 갖기 전에 동기적으로 플러시됩니다.

 

반응형
반응형

Create React App(CRA)는 리액트를 배우기에 간편한 환경이다 - (리액트 문서 참조)

CRA을 이용하면 웹팩이나 바벨같은 복잡한 설정을 몰라도 리액트를 쉽게 시작할 수 있다.

 

 

cra는 개발 환경을 설정하고, 최신 JavaScript를 사용하게 해주며, 좋은 개발 경험과 프로덕션 앱 최적화를 해줍니다.
Node 14.0.0 혹은 상위 버전 및 npm 5.6 혹은 상위 버전이 필요합니다.

 

 

 


1. node.js 설치

먼저, 초기 설정을 위해 node.js를 설치해준다.

(react는 node.js 기반의 라이브러리로, CRA는 node.js 서버에서 실행되기 때문에 node.js를 설치)

https://ko.legacy.reactjs.org/docs/create-a-new-react-app.html#create-react-app

(리액트 문서에는 Node 14.0.0 이상의 버전 및 npm 5.6 상위버전이 필요하다고 나와있다)

 

https://nodejs.org/ko/download/

 

 

 

 

 

위 링크를 누르면 아래와 같이 다운을 위한 페이지가 나온다.

개인 컴퓨터 사양과 환경에 맞게 다운로드해준다.

 LTS버전의 Windows Installer 64-bit를 설치해주었다.

 

 

 


 

 

CRA 실행

 cmd에서  npx create-react-app 앱이름(npm create-react-app 앱이름)

npx create-react-app my-app
cd my-app
npm start

 

 

앱이름(my-app)라는 폴더안으로 이동을 해서 npm start를 해주면 된다.

 

위 그림과 같이 생성이 된다.

 


 

 

public 폴더와 src 폴더 어떻게 사용되는 것인가?

 

 

 

public : 앱이 컴파일될 때 사용하지 않는 모든 것

  • 앱을 컴파일하는 데 필요하지 않은 요소들.
  • 절대경로 사용이 가능해진다!
  • import 해올 일이 있을 때, ../../이렇게 상대경로로 써주지 않아도 되고, 그냥 파일명 써주면 가능하다!
  • 정적파일을 담는 곳. 사용자가 직접 웹브라우저상으로 볼 수 있는 index.html같은 파일들, image 파일들이 담긴다.
  • 경로를 동적으로 참조해야 할 때 사용

 

 

src : 앱이 컴파일 될 때 사용하는 모든 것

  • 개발하면서 작업하는 파일 대부분을 넣는 폴더(index.js, 그 외 컴포넌트 같은 js파일, css파일 등)

예를 들어, 컴포넌트 안에서 사용하는 이미지는 src폴더에 있어야 하지만 파비콘과 같이 앱 밖에서 사용하는 이미지는 public 폴더에 있어야 한다.

 

 

이미지 파일 가져오기

📁 public
<img src="/image.jpg" />
📁 src
<img src={require('../../assets/image.jpg')} />

 

 

src폴더에 있는 이미지는 require를 통해 가져올 수 있다.

 

src폴더에서 require를 통해서 가져 온 이미지가 갖는 장점

  1. Scripts and stylesheets get minified and bundled together to avoid extra network requests.
    추가 네트워크 요청을 피하기 위해 스크립트와 스타일시트가 축소되고 함께 번들됩니다.
  2. Missing files cause compilation errors instead of 404 errors for your users.
    파일이 없으면 사용자에게 404 오류 대신 컴파일 오류가 발생합니다.
  3. Result filenames include content hashes so you don’t need to worry about browsers caching their old versions.
    결과 파일 이름에는 콘텐츠 해시가 포함되므로 브라우저가 이전 버전을 캐싱하는 것에 대해 걱정할 필요가 없습니다.

webpack's asset bundling을 사용하면 /src내의 파일들이 rebuilt된다!
-> rebuild를 빠르게 하기 src내부의 파일만 webpack에서 처리되므로 /src안에 모든 JS, CSS 파일을 넣어야 한다. 안그럼 webpack에서 볼 수 없음!

 

 

그렇다면 반대로 public 폴더로 가져온다면?

  1. 파일이 후처리(post-process)되거나 경량화(minify)되지 않음
  2. 파일 경로를 잘못 입력하거나 해당파일이 존재하지 않을 경우 컴파일 단계가 아닌 사용자가 접근할 때 404 오류를 응답받음
  3. 결과 파일명에 content hash가 포함되지 않기 때문에, 파일이 수정될 때마다 직접 파일명을 수정하거나 매개변수 쿼리를 추가해야 한다. 
    혹시 import를 안하고 인라인으로 src 내의 이미지 파일을 불러오고 싶다면!
 <img src={require("../../../assets/Logo.svg").default}/>



 

 

 


참고 
https://stackoverflow.com/questions/44643041/do-i-store-image-assets-in-public-or-src-in-reactjs
https://create-react-app.dev/docs/using-the-public-folder/#when-to-use-the-public-folder
https://bokjiho.medium.com/react-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90%EC%84%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EA%B2%BD%EB%A1%9C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0-public-src-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%B0%A8%EC%9D%B4-fddb4f455c2a

반응형
반응형

cra로 생성하지 않고 vite로 생성하는 이유

cra는 webpack로 번들링하고 코드가 바뀌면 모든 자바스크립트 코드를
새로 번들링 해서 앱이 커질수록 HMR(Hot Module Reloading)이 느려짐


Vite는 esbuild를 이용해서 변경된 부분만 새로 번들링하는 형식

첫 번째 실행해서 전체를 한 번을 번들링 그 이후 변경된 부분만 새로 번들링 


 

 

webpack은 Nodejs로 만들어졌고,

esbuild는 Go


다음 주소는
BRAD PEABODY의 글 URL입니다.
Server-side I/O Performance: Node vs. PHP vs. Java vs. Go

서버 I/O 성능 비교
https://www.toptal.com/back-end/server-side-io-performance-node-php-java-go
 

Server-side I/O Performance: Node vs. PHP vs. Java vs. Go | Toptal®

Clearly, Go is the winner here, followed by Java, Node and finally PHP.

www.toptal.com

 

 

 esbuild가 훨씬 빠르죠.

 

 

그리고 CRA는 개발 서버로 express 서버

반면 Vite는 Koa라는 조그마한 서버

여기서도 리소스 차이가 발생

 

 

Vite가 변경된 코드만 번들링 하는 원리는 Native ESM 모듈을 통해 import나 export 부분을 유심히 관찰하고

어떤 특정 import나 export가 코드가 변경됐다면 그 부분만 번들링 하는 방식입니다.

 

실제 구현 방식은 변경된 지 않은 모듈은 304 코드인 "not modified"를 리턴해서 브라우저가 그냥 무시하는 방식

 

Vite는 CommonJS나 UMD 방식을 ESM 방식으로 컨버팅 합니다.

그래서 위와 같이 ESM의 변경된 부분만 선별이 가능한 거죠.

 

 


Vite 설정

Vite 설정은 npm이나 yarn 또는 pnpm으로도 설정할 수 있습니다.

먼저, 다음과 같이 터미널에 입력하십시오.

npm create vite@latest

or 

yarn create vite

or

pnpm create vite

위와 같은 화면이 나오는데요.

원하는 UI Framework을 골라주고, Javascript나 Typescript 중에 골라 주면 됩니다.

CRA 앱과 다른 점은 npm install을 해주지 않는다는 점입니다.

직접 해줘야 하는데요.

그럼, 매번 골라주지 말고 바로 실행할 수 있는 명령어를 알아보겠습니다.

# npm 6.x
npm create vite@latest my-vue-app --template vue

# npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

# yarn
yarn create vite my-vue-app --template vue

# pnpm
pnpm create vite my-vue-app --template vue

위 그림을 보시면 저는 npm 버전이 8.11.0 이기 때문에 '--'를 한번 꼭 더 써야 합니다.

예상대로 한 번에 실행되는 걸 볼 수 있습니다.

그리고 template에 올 수 있는 문장은 다음과 같습니다.

vanilla, vanilla-ts,
vue, vue-ts,
react, react-ts,
preact, preact-ts,
lit, lit-ts,
svelte, svelte-ts

이제 만들었던 폴더로 들어가서 npm install을 실행해서 Node 모듈을 설치하고 개발서버를 돌려볼까요?

cd vite-project

npm install

npm run dev

Vite는 개발서버 포트가 3000번이 아닙니다.

기본으로 5173포트를 쓰는데요.

브라우저를 열어서 접속해 볼까요?

참고로 127.0.0.1은 localhost와 같은 의미입니다.

그래서 localhost:5173으로 접속해도 결과는 똑같습니다.

반응형
반응형

일반 HTML 페이지에 react를 추가하는 방법도 있으나.

https://ko.legacy.reactjs.org/docs/add-react-to-a-website.html

 

웹사이트에 React 추가 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

이 방법이 제일 쉽게 React를 이미 만들어진 웹사이트에 추가하는 방법입니다.

그리고 언제나 도움이 될 것 같으면 더 많은 툴체인을 추가할 수가 있습니다.

 

 


 

 

 

 

React 팀의 추천 방법은 아래와 같습니다

  • React를 배우고 있거나 아니면 새로운 싱글 페이지 앱을 만들고 싶다면 Create React App.
  • 서버 렌더링 Node.js 웹사이트를 만들고 있다면 Next.js
  • 고정적인 콘텐츠 지향적 웹사이트를 만들고 있다면 Gatsby
  • 컴포넌트 라이브러리 혹은 이미 있는 코드 베이스에 통합을 한다면 더 유연한 툴체인.

 

 


Create React App

Create React App은 React 배우기에 간편한 환경입니다.

그리고 시작하기에 최고의 방법은 새로운 싱글 페이지 애플리케이션 입니다.

이것은 개발 환경을 설정하고, 최신 JavaScript를 사용하게 해주며, 좋은 개발 경험과 프로덕션 앱 최적화를 해줍니다. Node 14.0.0 혹은 상위 버전 및 npm 5.6 혹은 상위 버전이 필요합니다.

 

 

새로운 프로젝트를 만들기 위해 아래의 명령어를 실행합니다.

npx create-react-app my-app
cd my-app
npm start

 

첫 번째 줄의 ‘npx’는 실수가 아니며 npm 5.2+ 버전의 패키지 실행 도구입니다.

 

 

Create React App 은 백 앤드 로직이나 데이터베이스를 제어할 수 없습니다.

Create React App 은 프런트 앤드 빌드 파이프라인만 생성하기 때문에 원하는 어떤 백엔드와도 함께 사용할 수 있습니다. Create React App는 Babel이나 webpack같은 build 도구를 사용하나, 설정 없이도 동작합니다.

프로덕션을 배포할 준비가 되었을 때, npm run build 를 실행하면 build 폴더 안에 제작한 앱의 최적화된 Build를 생성합니다. 

 

 

Next.js

Next.js는 인기 있는 경량의 프레임워크로 React로 만들어진 스태틱 서버 렌더링 애플리케이션입니다. 기본적으로 스타일링과 라우팅 해결책을 가지고 있으며, 사용자가 Node.js를 서버 환경으로 사용하고 있다고 생각합니다.


Next.js 정식 가이드

 

 

 

Gatsby

Gatsby는 정적 웹사이트를 React로 만들기에는 최고의 방법입니다. React 컴포넌트를 사용하게 해주지만 미리 렌더링 된 HTML과 CSS를 사용하여 가장 빠르게 로드됩니다.

 

정식 가이드 스타터 키트

 

 

 

 

더 유연한 툴체인

밑에 있는 툴체인은 조금 더 많은 선택과 다르기 쉬운 옵션입니다. 숙련된 사용자들에게 추천합니다.

  • Neutrino webpack의 장점과 React의 단순함과 미리 설정된 과 컴포넌트를 합친 것입니다.
  • Nx는 풀스택 모노레포 개발을 위한 도구이며, React, Next.js, Express 등을 기본적으로 지원합니다.
  • Parcel React와 함께 사용할 수 있고 빠르고 설정이 필요 없는 웹 애플리케이션 bundler입니다.
  • Razzle 서버 렌더링 프레임워크며 설정이 필요 없지만, Next.js보다 다루기 쉽습니다.

 

JavaScript build 툴체인은 주로 아래와 같이 구성되어있습니다

  • Yarn 혹은 npm같은 package 매니저는 서드 파티 패키지의 방대한 생태계를 활용할 수 있게 하며, 쉽게 설치하고 업데이트할 수 있게 합니다.
  • webpack 아니면 Parcel 같은 bundler는 코드를 모듈방식으로 작성할 수 있게 하고 이를 작은 package로 묶어서 로딩 시간을 최적화할 수 있습니다.
  • Babel 같은 컴파일러는 최신 JavaScript 코드를 구형 브라우저에도 실행되게 도와줍니다.

 

 

 


React와 ReactDOM 모두 CDN을 통해 사용할 수 있습니다.

<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

위의 코드는 개발용으로 적합하며 배포용 버전에는 적합하지 않습니다. React의 용량 및 성능 최적화된 배포용 버전은 아래와 같이 제공되고 있습니다.

<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

react와 react-dom의 특정 버전을 로딩하려면 18을 사용하고자 하는 버전 넘버로 대체하면 됩니다.


crossorigin 속성이 필요한 이유

CDN을 통해 React를 사용한다면, crossorigin 어트리뷰트(attribute)와 함께 사용하는 것을 권장합니다.

<script crossorigin src="..."></script>

또한 사용 중인 CDN이 Access-Control-Allow-Origin: * HTTP 헤더 설정을 사용하는지 확인하는 것이 좋습니다.

이를 통해 React 16 버전과 다음 버전에서 더 쉽게 에러 처리를 할 수 있습니다.

 

 

 


 

React의 각 배포 채널은 하나의 고유한 사용 경우를 위해 설계되었습니다.

  • Latest 는 안정적이고, 유의적인 React 배포입니다. npm에서 React를 설치할 때 얻는 것입니다. 이것은 이미 여러분이 사용하고 있는 채널입니다. 모든 사용자용 애플리케이션을 위해서는 이것을 사용해주세요.
  • Next 는 React 소스 코드 저장소의 main branch를 추적합니다. 이것을 다음 minor 유의적인 배포를 위한 배포 후보자라고 생각하세요. 이것을 React와 타사 프로젝트 간의 통합 테스트에 사용해주세요.
  • Experimental 는 실험용 API 및 stable 배포에서는 사용할 수 없는 기능이 포함됩니다. 이것은 또한 main branch를 추적하지만, 추가 기능 플래그가 켜져 있습니다. 배포하기 전에 배포가 예정된 기능들을 실험하는데 사용해주세요.

모든 배포는 npm에 게시되지만 오직 Latest만 의미론적 버전 관리를 사용합니다.

Prereleases는 (Next와 Experimental 채널에 있는 것) 내용의 hash와 커밋 날짜로부터 생성된 버전들을 가집니다,

 

예: Next를 위한 0.0.0-68053d940-20210623와 Experimental을 위한 0.0.0-experimental-68053d940-20210623.

 

사용자용 애플리케이션에 대해 공식적으로 지원되는 배포 채널은 Latest입니다.

 

Next와 Experimental 배포는 테스트 목적으로만 제공되며 배포 간에 동작이 변경되지 않는다는 보장을 제공하지 않습니다.

 

그것들은 Latest의 배포에 사용하는 유의적 버전원칙 프로토콜을 따르지 않습니다.

 

Latest 채널

Latest는 stable React 배포에 사용되는 채널입니다.

npm의 latest tag에 해당합니다. 실제 사용자들에게 제공되는 모든 React app에 권장되는 채널입니다.

 

어떤 채널을 사용해야 할지 잘 모르겠다면 Latest를 사용해야 합니다. 

 

Latest로 업데이트가 매우 안정적이다고 기대할 수 있습니다. 

 

 

Next 채널

Next 채널은 React 저장소의 main branch를 추적하는 prerelease 채널입니다.

 

 

Experimental 채널

Next와 마찬가지로 Experimental 채널은 React 저장소의 main branch를 추적하는 prerelease 채널입니다. Next와 달리, Experimental 배포는 광범위한 배포를 위해 준비되지 않은 추가 기능과 API를 포함합니다.

 

일반적으로, Next에 대한 업데이트는 Experimental에 대한 해당 업데이트와 함께 동반됩니다.

그것들은 동일한 소스 수정을 기반으로 하지만 다른 기능 플래그 세트를 사용하여 빌드됩니다.

 






출처 : https://ko.legacy.reactjs.org/docs/release-channels.html

반응형

+ Recent posts