원문보기

저는 2016년 부터 리액트를 가지고 작업을 해왔지만 여전히 어플리케이션 구조나 설계에 대한 하나의 모범 사례는 없는 것 같습니다.

마이크로 레벨의 모범 사례는 있었지만, 대부분의 팀들은 자신들만의 구조나 설계를 가지고 있었습니다.

물론 단 하나의 모범사례를 모든 비즈니스와 애플리케이션에 적용할 수는 없습니다. 하지만 생산적인 코드 베이스를 만들어 내기 위해 따라야할 몇 가지 일반적인 룰이 있습니다.

소프트웨어 아키텍쳐와 설계의 목적은 생산성 향상과 유연함을 유지하는 것입니다. 개발자들은 핵심 부분을 재작성하지 않고도 효율적으로 수정 작업을 할 필요가 있습니다.

이 글은 제가 리액트로 작업을 하면서 효율적임이 증명된 원칙과 룰의 모음입니다.

컴포넌트와 애플리케이션 구조, 테스트, 스타일링, 상태 관리와 데이터 가져오기에 대한 좋은 사례들의 개요를 잡았습니다. 몇몇 예제들은 지나치게 단순화 했지만 구현에 중점을 두는 것이 아니라 원칙에 중점을 두는 것이기 때문에 양해 바랍니다.

모든 것은 의견일 뿐 절대적인 것이 아닙니다. 소프트웨어를 작성하는 방법은 하나 이상입니다.

컴포넌트

Functional Component를 우선하기

Functional Component는 더 간단한 문법을 가지고 있습니다. 라이프사이클 메서드, 생성자나 판박이(boilerplate) 코드도 없습니다. 가독성을 잃지 않고도 같은 로직을 더 적은 문자수로 표현할 수 있습니다.

에러 바운더리 컴포넌트 구현을 제외하면 Functional Component를 우선시해야 합니다. 당신의 머릿속에서 고려해야 할 멘탈 모델의 크기가 훨씬 줄어듭니다.

// 👎 클래스 컴포넌트는 장황합니다
class Counter extends React.Component {
  state = {
    counter: 0,
  }

  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }

  handleClick() {
    this.setState({ counter: this.state.counter + 1 })
  }

  render() {
    return (
      <div>
        <p>counter: {this.state.counter}</p>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    )
  }
}

// 👍 함수형 컴포넌트는 읽기 쉽고, 유지 보수하기도 쉽습니다
function Counter() {
  const [counter, setCounter] = useState(0)

  handleClick = () => setCounter(counter + 1)

  return (
    <div>
      <p>counter: {counter}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  )
}

일관성 있는 컴포넌트 작성

컴포넌트들에 대해 동일한 스타일을 고수하세요. 헬퍼 함수들을 같은 위치에 두고, 같은 명명 패턴을 따르고, 같은 방법으로 export 하세요.

다른 방법들보다 더 이득이 되는 방법이란 것은 존재하지 않습니다.

파일 맨 아래쪽에서 익스포트하는 방법을 사용하건, 컴포넌트를 선언과 동시에 바로 익스포트하는 방법을 사용하건 상관없습니다. 다만 하나를 골라서 일관성 있게 적용하세요.

컴포넌트 이름짓기

항상 컴포넌트에 이름을 지으세요. React Dev Tools를 사용할 때나, 에러 스택 트레이스를 읽을 때 큰 도움이 됩니다.

또한 컴포넌트에 이름이 있다면 컴포넌트 개발 중에도 해당 컴포넌트가 어디에 있는지 찾기 쉬워집니다.

// 👎 이렇게 하지 마세요
export default () => <form>...</form>

// 👍 함수에 이름을 붙이세요
export default function Form() {
  return <form>...</form>
}

헬퍼 함수들을 조직하기

클로저를 유지할 필요 없는 헬퍼 함수들을 컴포넌트 바깥으로 이동하세요. 이상적인 이동 장소는 컴포넌트 정의 이전 위치 입니다. 위에서부터 아래로 쭉 읽어올 수 있게 됩니다.

컴포넌트 안의 노이즈를 감소시키고, 딱 필요한 것들만 컴포넌트 안에 존재할 수 있도록 합니다.

// 👎 클로저를 유지할 필요없는 함수를 굳이 컴포넌트 안쪽에 넣지 마세요
function Component({ date }) {
  function parseDate(rawDate) {
    ...
  }

  return <div>Date is {parseDate(date)}</div>
}

// 👍 컴포넌트 정의 위쪽에 헬퍼 함수를 두세요
function parseDate(date) {
  ...
}

function Component({ date }) {
  return <div>Date is {parseDate(date)}</div>
}

컴포넌트 정의 내에 최소한의 헬퍼 함수들만 유지합니다. 가능한한 많은 것들을 바깥 쪽으로 이동시키고 스테이트를 아규먼트로 전달합니다.

입력에만 의존하는 순수 함수로 로직을 구성하면 버그를 추적하고 기능을 확장하기가 쉬워집니다.

// 👎 헬퍼 함수들은 컴포넌트의 스테이트를 읽을 필요가 없습니다
export default function Component() {
  const [value, setValue] = useState('')

  function isValid() {
    // ...
  }

  return (
    <>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        onBlur={validateInput}
      />
      <button
        onClick={() => {
          if (isValid) {
            // ...
          }
        }}
      >
        Submit
      </button>
    </>
  )
}

// 👍 헬퍼 함수들을 추출해서 필요로 하는 값만 전달하세요
function isValid(value) {
  // ...
}

export default function Component() {
  const [value, setValue] = useState('')

  return (
    <>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        onBlur={validateInput}
      />
      <button
        onClick={() => {
          if (isValid(value)) {
            // ...
          }
        }}
      >
        Submit
      </button>
    </>
  )
}

마크업을 하드코드하지 않기

네비게이션, 필터, 리스트를 위해 마크업을 하드코드하지 마세요. 설정 객체(configuration object)와 루프를 사용하세요

이것은 마크업과 아이템의 변경을 한 곳에서 다룰 수 있게 해줍니다.

// 👎 하드코드된 마크업은 관리하기 어렵습니다
function Filters({ onFilterClick }) {
  return (
    <>
      <p>Book Genres</p>
      <ul>
        <li>
          <div onClick={() => onFilterClick('fiction')}>Fiction</div>
        </li>
        <li>
          <div onClick={() => onFilterClick('classics')}>
            Classics
          </div>
        </li>
        <li>
          <div onClick={() => onFilterClick('fantasy')}>Fantasy</div>
        </li>
        <li>
          <div onClick={() => onFilterClick('romance')}>Romance</div>
        </li>
      </ul>
    </>
  )
}

// 👍 루프와 설정 객체를 사용하세요
const GENRES = [
  {
    identifier: 'fiction',
    name: Fiction,
  },
  {
    identifier: 'classics',
    name: Classics,
  },
  {
    identifier: 'fantasy',
    name: Fantasy,
  },
  {
    identifier: 'romance',
    name: Romance,
  },
]

function Filters({ onFilterClick }) {
  return (
    <>
      <p>Book Genres</p>
      <ul>
        {GENRES.map(genre => (
          <li>
            <div onClick={() => onFilterClick(genre.identifier)}>
              {genre.name}
            </div>
          </li>
        ))}
      </ul>
    </>
  )
}

컴포넌트 길이

리액트 컴포넌트는 그저 props를 입력으로 받아 마크업을 돌려주는 함수일 뿐입니다. 그러므로 리액트 컴포넌트들도 함수와 동일한 소프트웨어 설계 원칙을 준수합니다.

보통 함수 하나가 너무 많은 일들을 하게되면, 로직을 다른 함수로 추출하고 그 함수를 호출하도록 합니다. 컴포넌트도 마찬가지입니다. 만약 컴포넌트가 너무 많은 기능을 가지고 있다면, 더 작은 컴포넌트를 만들고 그것을 호출하도록 합니다.

마크업 부분이 복잡하다면, 즉 루프와 조건 분기를 요구한다면, 더 작은 컴포넌트로 추출하세요.

데이터 전달과 커뮤니케이션은 props와 콜백에 의존하세요. 코드 줄 수는 객관적인 측정 수치가 아닙니다. 코드 줄 수 대신 추상화나 책임에 대해 생각하세요.

JSX에 주석 쓰기

더 명확히 밝혀야할 것이 있다면, 코드 블록을 열고 추가 정보를 제공하세요. 마크업도 로직의 일부이므로 더 명확히 밝힐 사항이 있다면 코드 블록을 열어 주석으로 추가 정보를 제공하면 됩니다.

function Component(props) {
  return (
    <>
      {/* If the user is subscribed we don't want to show them any ads */}
      {user.subscribed ? null : <SubscriptionPlans />}
    </>
  )
}

Error Boundary 사용하기

한 컴포넌트에서 에러가 발생했더라도, 전체 UI가 망가져서는 안됩니다. 전체 페이지가 망가졌음을 나타내야할 치명적인 에러가 드물게 발생하기는 합니다만, 대부분의 경우에서는 스크린에서 특정 엘리먼트만 숨기는게 나을 겁니다.

데이터를 다루는 함수 안에는 많은 try/catch 구문이 존재할 수도 있습니다. 에러 바운더리를 탑레벨에만 두지 마세요. 스크린의 각 엘레멘트를 에러 바운더리로 감싸서 분리시키면 연쇄적인 오류를 방지할 수 있습니다.

function Component() {
  return (
    <Layout>
      <ErrorBoundary>
        <CardWidget />
      </ErrorBoundary>

      <ErrorBoundary>
        <FiltersWidget />
      </ErrorBoundary>

      <div>
        <ErrorBoundary>
          <ProductList />
        </ErrorBoundary>
      </div>
    </Layout>
  )
}

Props 비구조화하기

대부분의 리액트 컴포넌트들은 함수일 뿐입니다. props를 받아서, 마크업을 리턴합니다. 일반적인 함수들은 전달인자를 그대로 전달받습니다. 리액트 컴포넌트도 같은 원칙을 적용하는 것이 이치에 맞습니다. props를 여기저기서 반복적으로 사용할 필요가 없다는 이야기입니다.

props를 비구조화해서 사용하지 않는 이유는 외부로부터 비롯된 것과 내부 상태를 구분하기 위해서입니다. 하지만 일반 함수는 전달인자와 내부 변수를 구별하지 않습니다. 불필요한 패턴을 만들어내지 마세요.

// 👎 컴포넌트 내에서 props를 반복해서 사용하지 마세요
function Input(props) {
  return <input value={props.value} onChange={props.onChange} />
}

// 👍 비구조화하여 직접 사용하세요
function Component({ value, onChange }) {
  const [state, setState] = useState('')

  return <div>...</div>
}

Props의 갯수

컴포넌트가 얼마나 많은 props를 받아야만 하는가 하는 질문은 다분히 주관적입니다. props의 갯수는 컴포넌트가 얼마나 많은 일을 하느냐와 연관되어 있습니다. 더 많은 props를 전달한다면 컴포넌트가 더 많은 책임을 갖고 있다는 것입니다.

높은 props 갯수는 컴포넌트가 너무 많은 일을 한다는 신호입니다.

저는 만약 5개 이상의 props가 있다면, 컴포넌트를 쪼개는 것을 고려합니다. 어떤 경우에는 그냥 데이터가 많은 것일 수도 있습니다. 예로 들면 인풋 필드 같은 경우에는 props를 많이 가질 수 있습니다. 그러나 그 외 경우에는 추출해야할 무언가가 있다는 신호입니다.

NOTE: 컴포넌트가 더 많은 props를 취한다면, rerender될 이유가 더 많다는 것입니다.

원시타입들 대신 오브젝트를 전달하기

props의 숫자를을 제한하는 한가지 방법은 원시타입들 대신에 오브젝트를 전달하는 것입니다. username, emaill, settings를 하나씩 하나씩 전달하는 것보다, 그룹으로 묶어서 전달하는 것이 낫습니다. 이 방법은 또한 user가 추가적인 필드를 가지게 될 때, 필요로 하는 변경의 양을 줄일 수 있습니다.

타입스크립트를 사용한다면 더 쉽게 만들 수 있습니다.

// 👎 서로 연관된 값을 하나씩 전달하지 마세요
<UserProfile
  bio={user.bio}
  name={user.name}
  email={user.email}
  subscription={user.subscription}
/>

// 👍 대신 하나로 묶은 오브젝트를 사용하세요
<UserProfile user={user} />

조건부 렌더링

어떤 상황에서 숏 서킷 연산자를 사용해 조건부 렌더링을 하는 것은 역효과를 내거나, 의도치 않은 0 을 UI에 노출하게 됩니다. 이를 피하기 위해서 기본적으로 삼항 연산자를 사용하세요. 유일한 단점은 삼항연산자가 더 장황하다는 것입니다.

숏 서킷 연산자를 사용하는 것은 코드의 양을 줄여줍니다. 코드의 양을 줄이는 것은 항상 훌륭한 일입니다. 삼항 연산자는 더 장황합니다. 하지만 별로 잘못될 일이 없죠. 거기에다가 조건문을 변경 해야 하는 경우에 코드 변경량이 더 적습니다.

// 👎 숏 서킷 연산자를 사용하는것을 피하도록 노력해 보세요
function Component() {
  const count = 0

  return <div>{count && <h1>Messages: {count}</h1>}</div>
}

// 👍 대신 삼항 연산자를 쓰세요
function Component() {
  const count = 0

  return <div>{count ? <h1>Messages: {count}</h1> : null}</div>
}

중첩된 삼항 연산자 피하기

첫 번째 레벨의 삼항 연산자 이후의 삼항 연산자는 정말 읽기 힘듭니다. 시간과 공간의 낭비를 줄일 수 있다고 하더라도 말이죠. 당신의 의도를 명확하고 확실하게 나타내는 것이 더 낫습니다.

// 👎 JSX 내 중첩된 삼항 연산자는 읽기 어렵습니다
isSubscribed ? (
  <ArticleRecommendations />
) : isRegistered ? (
  <SubscribeCallToAction />
) : (
  <RegisterCallToAction />
)

// 👍 내부 컴포넌트를 하나 만들어서 로직을 명확히 드러내세요
function CallToActionWidget({ subscribed, registered }) {
  if (subscribed) {
    return <ArticleRecommendations />
  }

  if (registered) {
    return <SubscribeCallToAction />
  }

  return <RegisterCallToAction />
}

function Component() {
  return (
    <CallToActionWidget
      subscribed={subscribed}
      registered={registered}
    />
  )
}

리스트들은 별도 컴포넌트로 분리하기

map 함수를 통해서 아이템 목록을 반복하는 것은 일상다반사입니다. 그러나 많은 마크업을 가지고 있는 컴포넌트에서 추가적인 들여쓰기(indentation)와 map 문법은 가독성에 전혀 도움이 되지 않습니다.

만약 여러분이 엘레멘트들을 map으로 반복해야할 때라면, 별도의 목록 컴포넌트로 추출하세요. 비록 원래 컴포넌트 내에 마크업이 많지 않더라도 말입니다. 부모 컴포넌트는 세부 사항에 대해 알 필요가 별로 없습니다. 그냥 목록을 표시하기만 하면 됩니다.

컴포넌트의 주요 책임이 마크업을 반복하고 표시하는 것일 경우에만 마크업에 루프를 두어도 됩니다. 컴포넌트 당 하나의 맵핑만을 두도록 노력해보세요, 하지만 마크업이 길거나 복잡한 경우엔 무조건 리스트 컴포넌트로 추출하세요.

// 👎 나머지 마크업과 루프를 함께 작성하지 마세요
function Component({ topic, page, articles, onNextPage }) {
  return (
    <div>
      <h1>{topic}</h1>
      {articles.map(article => (
        <div>
          <h3>{article.title}</h3>
          <p>{article.teaser}</p>
          <img src={article.image} />
        </div>
      ))}
      <div>You are on page {page}</div>
      <button onClick={onNextPage}>Next</button>
    </div>
  )
}

// 👍 리스트를 하나의 컴포넌트로 추출하세요
function Component({ topic, page, articles, onNextPage }) {
  return (
    <div>
      <h1>{topic}</h1>
      <ArticlesList articles={articles} />
      <div>You are on page {page}</div>
      <button onClick={onNextPage}>Next</button>
    </div>
  )
}

비구조화 시 props에 기본값 할당하기

props의 기본값을 지정하는 한가지 방법은 컴포넌트에 defaultProps 프로퍼티를 덧붙이는 것입니다. 이는 컴포넌트 함수와 그 전달인자의 값이 같은 위치에 있지 않게 된다는 뜻입니다.

props를 비구조화할 때, 직접 기본값을 지정하는 것을 우선시하세요. 이 방법을 이용하면 코드를 읽을 때, 불필요하게 코드의 윗부분 아랫부분을 왔다갔다 할 필요 없이, 전달인자가 정의된 곳에서 기본값을 읽을 수 있게 만들기 때문에 코드가 읽기 쉬워집니다.

// 👎 함수 바깥에 props의 기본값을 정의하지 마세요
function Component({ title, tags, subscribed }) {
  return <div>...</div>
}

Component.defaultProps = {
  title: '',
  tags: [],
  subscribed: false,
}

// 👍 전달인자 목록에 기본값을 함께 정의하세요
function Component({ title = '', tags = [], subscribed = false }) {
  return <div>...</div>
}

중첩된 렌더 함수 피하기

컴포넌트나 로직에서 마크업을 추출해야할 필요가 있을 때, 같은 컴포넌트 내에 추출한 마크업을 함께 두지 마세요. 컴포넌트는 그저 함수일 뿐이고, 쉽게 부모 컴포넌트 내부에 중첩된 함수로 정의해 둘 수 있습니다.

하지만 그렇게 하면 중첩된 함수가 부모 컴포넌트의 상태와 데이터에 접근할 수 있게 됩니다. 이것이 코드를 더욱 읽기 어렵게 만듭니다. 이 모든 컴포넌트 사이에서 이 함수가 과연 무슨 일을 하는것일까? 하는 의문을 들게 만드는 것이죠

부모 내부에 컴포넌트를 정의하지 말고 따로 컴포넌트로 분리하세요. 클로저를 사용하기 보다는 이름을 붙이고 props를 전달하세요.

// 👎 렌더 함수 내에 중첩된 렌더 함수를 두지 마세요
function Component() {
  function renderHeader() {
    return <header>...</header>
  }
  return <div>{renderHeader()}</div>
}

// 👍 별도의 컴포넌트로 추출하세요
import Header from '@modules/common/components/Header'

function Component() {
  return (
    <div>
      <Header />
    </div>
  )
}

상태 관리

리듀서 사용하기

종종 상태(State) 변화를 관리하는 더 강력한 방안이 필요할 때가 있습니다. 그럴 때 외부 라이브러리로 눈을 돌리기 전에 먼저 useReducer 로 시작해보세요. useReducer는 복잡한 상태 관리를 다루는 훌륭한 메커니즘이면서, 서드파티 의존성을 요구하지도 않습니다.

리액트의 Context, 타입 스크립트, useReducer의 조합은 정말 강력합니다. 하지만 불행히도, 널리 쓰이진 않고 있습니다. 사람들은 아직도 서드파티 라이브러리에 손을 대고 있습니다.

만약 조각조각 분리된 여러 상태가 필요해진 상황이라면, 상태를 리듀서로 옮기는 것을 고려해보세요.

// 👎 조각조각 분리된 상태가 너무 많습니다. 이렇게 사용하지 마세요
const TYPES = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large'
}

function Component() {
  const [isOpen, setIsOpen] = useState(false)
  const [type, setType] = useState(TYPES.LARGE)
  const [phone, setPhone] = useState('')
  const [email, setEmail] = useState('')
  const [error, setError] = useSatte(null)

  return (
    ...
  )
}

// 👍 대신 리듀서로 통합하세요
const TYPES = {
  SMALL: 'small',
  MEDIUM: 'medium',
  LARGE: 'large'
}

const initialState = {
  isOpen: false,
  type: TYPES.LARGE,
  phone: '',
  email: '',
  error: null
}

const reducer = (state, action) => {
  switch (action.type) {
    ...
    default:
      return state
  }
}

function Component() {
  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    ...
  )
}

HOC와 Render props보다 Hooks를 우선시하기

종종 기존 컴포넌트의 기능을 강화(enhance)하거나, 기존 컴포넌트에 외부 상태에 접근할 수 있는 능력을 주고 싶은 경우가 있습니다. 보통 세 가지 방법이 사용됩니다. 바로 higher order components(HOCs), render props, hooks 입니다.

Hooks는 이러한 컴포넌트의 능력을 조합하는 데 가장 효과적인 방안임이 증명되었습니다. 철학적인 관점에서, 컴포넌트는 다른 함수들을 사용하는 함수입니다. Hooks를 사용하면 서로 충돌하는 일 없이 외부로부터 기능을 가져와서 덧붙일 수 있게 해줍니다. hooks의 갯수가 상당히 늘어나더라도, 각각의 값들이 어디로부터 온건지 쉽게 알 수 있습니다.

HOCs를 이용하는 경우 props로 값들이 전달됩니다. 이 값들은 부모 컴포넌트로부터 온 것인지, 아니면 HOCs로부터 전달된 것인지 알기 어려워집니다. 또한, 여러 props를 체이닝하는 것은 에러의 원인으로 알려져 있습니다.

Render props는 깊은 들여쓰기와 좋지 못한 가독성을 야기합니다. 마크업 트리에 여러 컴포넌트를 render props로 중첩시키는 것은 보기에도 좋지 않습니다. 또한 마크업 자체는 값들만을 노출하므로 로직을 따로 작성하거나, 아래로 전달해야 합니다.

hooks를 사용하면 추적하기 쉽고, JSX를 방해하지 않는 간단한 값들만으로 작업할 수 있습니다.

// 👎 render props 사용을 피하세요
function Component() {
  return (
    <>
      <Header />
      <Form>
        {({ values, setValue }) => (
          <input
            value={values.name}
            onChange={e => setValue('name', e.target.value)}
          />
          <input
            value={values.password}
            onChange={e => setValue('password', e.target.value)}
          />
        )}
      </Form>
      <Footer />
    </>
  )
}

// 👍 단순함과 가독성을 위한 훅을 사용하세요
function Component() {
  const [values, setValue] = useForm()

  return (
    <>
      <Header />
      <input
        value={values.name}
        onChange={e => setValue('name', e.target.value)}
      />
      <input
        value={values.password}
        onChange={e => setValue('password', e.target.value)}
      />
    )}
      <Footer />
    </>
  )
}

데이터 가져오기용 라이브러리 사용하기

종종 상태 안에서 관리하려는 데이터를 API로부터 가져오는 경우가 있다. 메모리에 데이터를 유지할 필요가 있으며, 다양한 곳에서 접근하고 업데이트할 수 있어야한다.

React Query 같은 모던 데이터 페칭 라이브러리는 외부 데이터를 관리하기에 충분한 메커니즘을 제공합니다. 캐시할 수도 있고, 캐시를 무효화 한다음 새로 받아올 수도 있습니다. 받아오기만 하는게 아니라 데이터를 보내거나, 아니면 리프레시로 인해 추가적인 데이터 조각을 가져오는 데에도 사용할 수 있습니다.

Apollo 같은 GraphQL 클라이언트를 사용하면 더 쉽습니다. 클라이언트 상태 개념이 내장되어 있기 때문입니다.

상태 관리 라이브러리

아마 대부분의 경우에 상태 관리 라이브러리는 필요없을 겁니다. 아주 복잡한 상태를 관리하는 커다란 애플리케이션에만 필요하기 때문입니다. 이 주제에 대한 다양한 가이드가 있으므로, 살펴볼 만한 두 라이브러리만 언급하고 넘어가겠습니다.

RecoilRedux 입니다.

컴포넌트 멘탈 모델

Container와 Presentational

현재 우세한 사고방식은 컴포넌트를 두 그룹으로 나누는 것입니다. 바로 presentational 컴포넌트와 container 컴포넌트로 말이죠. 각각 스마트 컴포넌트(컨테이너)와 바보 컴포넌트(프레젠테이셔널)라고도 불립니다.

이 아이디어는 어떤 컴포넌트는 어떠한 상태나 기능을 같고 있지 않는다는 것입니다. 프레젠테이션 컴포넌트는 그저 부모로부터 props를 전달받아 호출되기만 합니다. 컨테이너 컴포넌트는 비즈니스 로직을 담고 있으며, 데이터를 페칭하고 상태를 관리합니다.

이 멘탈 모델은 백엔드 애플리케이션의 MVC 구조입니다. 어디에서나 작동하기에 충분히 일반적이며 잘못될 일이 별로 없습니다.

그러나, 현대적인 UI 애플리케이션에서는 이 패턴만으로는 부족합니다. 몇 가지의 컴포넌트가 모든 논리를 가지고 있으면, 부풀어 오르기 마련입니다. 해당 컴포넌트들은 너무나 많은 책임을 가지고 있어서, 관리하기가 어려워집니다.

Stateless & Stateful

컴포넌트를 상태가 있는 컴포넌트와 상태가 없는 컴포넌트로 나누는 것입니다. 위에서 언급한 Container & Presentation 멘탈 모델은 몇몇 소수의 컴포넌트들로만 커다란 규모의 복잡성을 다루어야만 한다는 것을 의미합니다. 그렇게 하는 대신, 우리는 앱 전체에 이 복잡성을 쪼개어 퍼트려야 합니다.

데이터는 사용되는 위치와 가까운 곳에 있어야 합니다. GraphQL client를 사용할 때, 데이터를 보여주는 컴포넌트에서 해당 데이터를 가져옵니다. 탑 레벨의 컴포넌트가 아니어도요. Container 에 대해서는 생각하지 마세요. 대신 Responsibilities 에 대해서 생각하세요. 논리적으로 가장 적합한 위치에 상태를 두는 것에 대해 고려하세요.

예를 들어, <Form /> 컴포넌트는 해당 form 이 필요한 데이터를 가져야만 합니다. <Input /> 컴포넌트는 값들과 변경을 알릴 콜백을 전달해야 합니다. <Button /> 컴포넌트는 눌러졌을 때 폼에 통지하도록 하고, 폼은 일어난 일에 대해 핸들링해야 합니다.

폼의 유효성 검증은 누가 해야 할까요? 인풋 필드의 책임일까요? 이것은 당신의 애플리케이션 내 컴포넌트가 비즈니스 로직을 인식하게됨을 의미합니다. 폼에 에러가 발생하면 어떻게 통지해야 할까요? 어떻게 에러 상태가 갱신되야 할까요? 폼이 그것을 알아야 할까요? 에러가 있지만 서브밋을 시도한다면 무슨 일이 일어날까요?

이와 같은 질문에 직면했다면 책임이 뒤섞이고 있음을 인식해야 합니다. 위 경우에는 인풋 필드는 stateless로 남겨두고 폼으로부터 에러 메시지를 전달받도록 하는 것이 더 나을 것입니다.

애플리케이션 구조

라우트나 모듈을 기준으로 그룹화하기

컨테이너 컴포넌트와 프레젠테이션 컴포넌트로 그룹을 만드는 것은 애플리케이션 구조를 알아보기 어렵게 합니다. 컴포넌트가 어디에 속하는지 이해하게 하려는 것은 친숙함을 필요로 합니다.

모든 컴포넌트가 전부 동등하지는 않아서, 글로벌하게 사용되는 몇몇 컴포넌트와 애플리케이션의 특정 부분에서만 사용되는 컴포넌트로 그룹화하는 구조가 있습니다. 이 구조는 가장 작은 규모의 프로젝트에서는 잘 동작합니다. 그러나 이 구조에서는 점점 컴포넌트가 늘어나게 되면서 관리하기가 어려워지는 단점이 있습니다.

// 👎 기술적인 세부사항을 기준으로 그룹화하지 마세요
├── containers
|   ├── Dashboard.jsx
|   ├── Details.jsx
├── components
|   ├── Table.jsx
|   ├── Form.jsx
|   ├── Button.jsx
|   ├── Input.jsx
|   ├── Sidebar.jsx
|   ├── ItemCard.jsx

// 👍 모듈/도메인 기준으로 그룹화하세요
├── modules
|   ├── common
|   |   ├── components
|   |   |   ├── Button.jsx
|   |   |   ├── Input.jsx
|   ├── dashboard
|   |   ├── components
|   |   |   ├── Table.jsx
|   |   |   ├── Sidebar.jsx
|   ├── details
|   |   ├── components
|   |   |   ├── Form.jsx
|   |   |   ├── ItemCard.jsx

라우트나 모듈 별로 그룹화하는 것으로 시작하세요. 애플리케이션이 변화하고 성장하는 것을 지원하는 구조입니다. 요점은 애플리케이션이 성장하면서 특정 아키텍쳐 부분이 너무 커져버리게 하지 않는 것입니다. 컨테이너 컴포넌트와 프레젠테이션 컴포넌트 기반으로 그룹화할 때 자주 그런 일이 일어납니다.

모듈 기반의 아키텍쳐는 확장하기 쉽습니다. 별도의 복잡성을 증가시키지 않고 그저 탑 레벨의 피쳐 모듈 하나를 추가하기만 하면 됩니다.

컨테이너 컴포넌트 프레젠테이션 컴포넌트 기반의 구조는 틀리지 않았습니다면 너무 일반적입니다. 코드를 읽는 독자에게 그저 리액트 컴포넌트로 구성되어있다는 것 밖에 알려주지 않습니다.

공통 모듈 작성하기

버튼이나 인풋, 카드 같은 컴포넌트들은 모든 곳에서 사용됩니다. 모듈 기반의 애플리케이션 구조를 가져간다고 하더라도, 이러한 컴포넌트들을 따로 추출해두는 것이 좋습니다.

그렇게 한다면 스토리북을 사용하지 않더라도 어떤 공통 컴포넌트가 있는지 확인할 수 있어서, 중복을 피할 수 있습니다. 당신도 아마 당신 팀의 모든 팀원들이 자기들만의 버전의 버튼 컴포넌트를 가지는 것을 원하지는 않을 것입니다. 불행히도 잘못 구성된 프로젝트 때문에 자주 일어나는 일입니다.

절대 경로 사용하기

구조 변경을 쉽게 만들어두는 것이 프로젝트 구조화의 기본입니다. 절대 경로는 컴포넌트를 옮겨야할 필요가 있을 때 더 적게 바꿀 수 있음을 의미합니다. 또한 임포트한 모듈을 어디로부터 가져온 것인지 쉽게 알 수 있게 됩니다.

// 👎 상대경로를 사용하지 마세요
import Input from '../../../modules/common/components/Input'

// 👍 절대적인것은 변하지 않습니다
import Input from '@modules/common/components/Input'

보통 저는 @ 프리픽스를 사용하지만 ~를 사용하는 경우도 보았습니다.

외부 컴포넌트를 한 번 감싸기

너무 많은 서드파티 컴포넌트를 직접 임포트하는 것을 하지 않도록 시도해보세요. 서드파티 라이브러리들을 한번 감싼 어댑터를 만들어서 필요한 경우에 API를 수정할 수 있도록 하세요. 또한 다른 서드 파티 라이브러리로 교체할 경우 어댑터 한 곳만 수정하게 되는 이득이 있습니다.

Semantic UI같은 컴포넌트나 그 밖의 유틸리티 컴포넌트도 마찬가지 입니다. 가장 간단한 방법은 임포트한 컴포넌트를 바로 다시 익스포트하는 모듈을 만드는 것입니다.

감싼 컴포넌트를 사용하는 컴포넌트는 감싼 컴포넌트가 어떤 라이브러리를 사용하고 있는지 알 필요가 없습니다. 그냥 감싼 컴포넌트가 노출하는 것만 알면 됩니다.

// 👎 직접적으로 임포트하지 마세요
import { Button } from 'semantic-ui-react'
import DatePicker from 'react-datepicker'

// 👍 컴포넌트를 감싸서 노출시킨 다음 내부 모듈로 레퍼런스하게 하세요
import { Button, DatePicker } from '@modules/common/components'

컴포넌트를 자체 폴더로 만들어주기

저는 리액트 애플리케이션을 작성할 때 각각의 모듈 폴더마다 컴포넌트 폴더를 만들어 둡니다. 컴포넌트를 만들 때 가장 먼저 하는 일이 폴더를 만드는 일입니다. 스타일이나 테스트같이 추가적인 파일들이 필요하다면 그 폴더에 넣습니다.

일반적인 사례는 index.js 파일로 리액트 컴포넌트를 익스포트하는 것입니다. 그렇게 하면 import From from 'components/UserForm/UserForm' 과 같이 import 경로가 반복되는 일이 없게 됩니다. 그리고 또 하나. 여러 index.js 파일이 열려 있을 때 혼동되지 않도록 컴포넌트 이름으로 파일을 두어야 합니다.

// 👎 여러 컴포넌트 파일들을 함께 두지 마세요
├── components
    ├── Header.jsx
    ├── Header.scss
    ├── Header.test.jsx
    ├── Footer.jsx
    ├── Footer.scss
    ├── Footer.test.jsx

// 👍 컴포넌트만의 폴더를 만들어서 옮기세요
├── components
    ├── Header
        ├── index.js
        ├── Header.jsx
        ├── Header.scss
        ├── Header.test.jsx
    ├── Footer
        ├── index.js
        ├── Footer.jsx
        ├── Footer.scss
        ├── Footer.test.jsx

성능

섣부르게 최적화하지 않기

어떠한 종류의 최적화라도, 하기 전에 꼭 최적화의 이유를 확실히 해야 합니다. 애플리케이션에 영향이 없는 한 모범 사례를 맹목적으로 따르는 것은 노력을 낭비하는 것입니다.

예. 어떤 특정한 것들에 대한 인식은 중요합니다. 하지만 읽기 쉽고 유지 보수하기 쉬운 컴포넌트를 구축하는 것을 성능보다 우선 순위를 높게 두어야 합니다. 잘 작성된 코드는 향상시키기도 편합니다.

만약 애플리케이션에서 퍼포먼스 문제가 발생했다면, 측정한 다음에 문제의 원인을 파악해야 합니다. 번들 사이즈가 엄청 큰 상황에서 리렌더링 회수를 줄이는 것은 거의 의미가 없습니다.

성능 문제의 원인을 파악한 다음에는 영향이 큰 순서대로 수정하세요.

번들 사이즈 살펴보기

브라우저로 전달되는 자바스크립트의 양은 성능에서 가장 중요한 요소입니다. 당신의 애플리케이션이 엄청나게 빠를 수도 있지요. 하지만 4MB의 자바스크립트가 로드되기 전까지는 아무도 그 사실을 알지 못할 겁니다.

자바스크립트를 하나의 번들로 만들어서 전달하지 마세요. 애플리케이션을 라우트 레벨 혹은 더 낮은 레벨로 쪼개세요. 가능한한 적은 양의 자바스크립트를 전송하도록 만드세요.

백그라운드에서 나머지 번들을 로드하거나 사용자가 의도를 보일때 해당 번들을 로드하세요. 만약 PDF를 다운로드하는 기능의 버튼이 있다면, 해당 버튼에 마우스 커서가 호버되기 전까지 PDF 관련 라이브러리를 로드하지 않을 수도 있습니다.

리렌더링 - 콜백, 배열 그리고 객체

불필요한 리렌더링의 양을 줄이는 것은 좋은 시도입니다. 하지만 불필요한 리렌더를 줄이는 것이 애플리케이션에 가장 큰 영향을 끼치는 것은 아니라는 것을 염두에 두세요.

가장 일반적인 충고는 인라인 콜백 함수를 props로 전달하는 것을 피하라는 것입니다. 인라인 콜백 함수 하나를 사용하면 각각의 렌더링마다 새로운 함수가 만들어지는 것을 뜻하고, 이는 하위의 렌더링을 유발합니다. 지금 제가 제시하는 접근 방식 때문에, 저는 콜백으로 인한 성능 문제를 겪은 적이 없습니다.

실제로 성능 문제를 겪고 있다면 클로저가 원인인지 확인하고 제거해보세요. 하지만 그로 인해 코드 가독성을 떨어뜨리거나, 더 장황하게 만들지는 마세요.

props로 배열이나 객체를 인라인으로 전달하는 것도 같은 종류의 문제를 유발합니다. 레퍼런스가 매번 갱신되므로 리렌더링이 유발됩니다. 만약 고정된 배열을 전달하는 경우에는 컴포넌트 정의의 바깥쪽에 해당 배열을 상수로 정의해두고 그 레퍼런스를 전달하도록 만들어보세요.

테스팅

스냅샷 테스트에 의존하지 않기

제가 리액트로 작업하기 시작했던 2016년부터 지금까지 스냅샷 테스트가 컴포넌트 문제를 발견한 상황은 단 하나뿐입니다. 전달인자 없이 new Date() 를 호출해서 항상 기본값이 현재 시간으로 설정되었던 문제입니다.

이 외에도 스냅샷은 컴포넌트가 변경될 때 빌드가 실패하게되는 원인이 될 뿐입니다. 보통의 작업 흐름은 컴포넌트를 변경한 다음 스냅샷 테스트가 실패한 것을 확인하고, 스냅샷을 업데이트 한 뒤 계속 진행하는 것입니다.

오해하지 마세요. 스냅샷 테스트는 좋은 정밀 테스트(Sanity Test) 입니다. 하지만 컴포넌트 레벨의 테스트를 대체하기에는 좋지 않습니다. 저는 이저 더이상 스냅샷 테스팅을 하지 않습니다.

올바르게 렌더링되는지 테스트하기

여러분이 테스트할때 주로 테스트해야하는 것은 컴포넌트가 기대한대로 동작하는지 검사하는 것이여야 합니다. default props로 올바르게 렌더링 되는지, 전달된 props로 렌더링 되는지를 테스트하게 하세요.

주어진 인풋(props)에 따라 올바른 결과(JSX)를 반환해야 합니다. 스크린 상의 필요한 모든 것을 검사하세요.

상태와 이벤트를 검사하기

내부 상태를 가진 컴포넌트(stateful component)는 대부분 이벤트에 대한 응답을 통해 내부 상태가 변경됩니다. 이벤트를 시뮬레이션해서 컴포넌트가 맞게 응답하는지 확인하세요.

핸들러 함수가 호출되었고, 올바른 인수가 전달되었는지 확인하세요. 그 결과로 내부적인 상태가 올바르게 설정되었는지 확인하세요.

엣지 케이스 테스트

기본적인 테스트를 끝냈다면, 몇몇 엣지 케이스 다루는 테스트 케이스를 추가하세요.

빈 배열을 전달해서 확인없이 인덱스를 접근하는 일이 없는지를 검사하고, API 호출시 에러를 던지고 컴포넌트가 그것을 핸들링하는지 검사한다는 의미입니다.

통합 테스트 작성하기

통합 테스트는 전체 페이지나 커다란 컴포넌트를 검사하는 것을 의미합니다. 추상가 잘 동작하는지 테스트합니다. 통합 테스트는 애플리케이션이 예상대로 잘 동작하는지 확인하는 가장 자신감을 가질 수 있는 방법입니다.

컴포넌트 하나하나가 잘 동작하고, 단위 테스트가 잘 통과하더라도, 이들 컴포넌트를 통합하는 것에는 문제가 있을 수 있습니다.

스타일링

CSS-in-JS 사용하기

많은 사람들이 동의하지 않을 만한 논란이 있는 의견입니다만, 컴포넌트에 대한 모든 것을 자바스크립트로 표현할 수 있기 때문에 Styled Components 혹은 Emotion을 사용하고 싶습니다. 유지보수할 파일이 하나 줄어들고 CSS 컨벤션에 대해 생각할 필요가 없어집니다.

리액트의 논리적인 단위는 컴포넌트이므로 관심사 분리 측면에서 관련된 모든 것을 소유해야만 합니다.

NOTE: SCSS, CSS modules, Taliwind 같은 스타일링이 잘못된 옵션이란 것은 아닙니다. 그저 제가 추천하는 방법이 CSS-in-JS라는 것입니다.

스타일과 컴포넌트를 함께 유지하기

CSS-in-JS가 적용된 컴포넌트에서는 하나의 파일에 스타일과 함께 있는 게 일반적입니다. 이상적으로는 컴포넌트와 그 컴포넌트가 사용하는 것들이 한 파일에 있는 것입니다.

하지만 파일이 너무 길어지게 된다면 스타일만을 위한 파일을 만들어서 컴포넌트 파일 옆에 둘 수 있습니다. 이러한 패턴을 Spectrum 같은 오픈 소스 프로젝트에서 본 적이 있습니다.

데이터 가져오기

데이터 가져오기용 라이브러리 사용하기

리액트는 API로부터 데이터를 가져오거나 데이터를 업데이트하는 방법에 대해 의견을 제시하지 않습니다. 각 팀들은 보통 API 통신을 위한 자신들만의 비동기 함수의 구현을 서비스에 포함합니다.

이러한 방식은 로딩 상태를 관리하고 http 오류를 자체적으로 처리해야 함을 의미하고, 이는 많은 판박이 코드(boilerplate code)를 가진 장황한 코드를 유발합니다.

이러한 방법 대신 React QuerySWR 과 같은 라이브러리를 사용하세요. 컴포넌트 라이프사이클의 일부분에 딱 맞는 서버의 통신 방법을 제공합니다. 바로 훅이죠.

이러한 라이브러리는 자체적으로 로딩이나 에러 상태를 관리하고, 캐시를 만들어줍니다. 우리는 그냥 로딩 상태 에러 상태, 캐시를 다루기만 하면 되죠. 또한 데이터 처리를 위한 상태 관리 라이브러리의 사용을 없애줍니다.

원문보기

예전에 React와 Redux로 옮겨가면서 배운 몇 가지 교훈에 관한 포스트를 작성했었다.

그 뒤로 꽤 긴 시간이 지났지만 아직도 React와 그 친구들을 사용하고 있고, 더 공유해보고 싶은 것이 있다. 무엇이 가장 큰 고통이며 어떻게 그것을 이겨낼 것인가 하는 것 말이다. 그것은 바로 forms를 구현하는 것이다.

많은 웹 애플리케이션들은 폼 기반이다. 당신의 플랫폼을 사용자에게 제공하려는 목적으로 사용자로 하여금 폼을 채우고 폼 컴포넌트를 조작하게끔 한다. 곧, 폼 컴포넌트를 React에서는 어떻게 사용하고 작성해야 하는지에 대한 의문이 생긴다. 그리고 React와 Redux가 어떻게 함께 동작하는지도 말이다. 예를 들어 다음과 같은 의문들이다.

  • # 컴포넌트는 어떻게 동작해야 하는가?
  • # 데이터를 어디에 유지해야 하는가?
  • # 유효성 검사는 어떻게 수행하는가?
  • # 폼의 상태와 그 변화는 어떻게 탐지하는가?

Bizzabo에서는 꽤 많은 페이지들이 폼 기반이다. 이 페이지들은 꽤 오래전부터 해왔던 것이기 때문에 Backbone.js를 사용했던 동안 어떤 도전들이 있었고 어떤 가이드가 필요한지 알고 있었다. 그러나 React는 또다른 경기장이였고, 사용자가 수월하게 폼의 내용을 채우게 하기 위해서는 Backbone을 사용한 폼이나 React를 사용한 폼이나 차이점을 느끼지 못해도록 해야 한다. 그러나 한편으로는 이 모든 물음들에 대한 대답을 위해 폼을 사용하는 방법을 완전히 새롭게 설계했다.

이 물음들에 대답하기에 앞서, 결과부터 이야기하자면 우리는 redux-form을 사용했다. Erik Rasmussen에 의해 만들어진, redux 위에서 동작하는 폼 상태 관리 라이브러리로, 매우 잘 관리되고 있다.

# 컴포넌트가 어떻게 동작해야 하는가?

시스템 내의 모든 폼 컴포넌트는 사용자 인터페이스 동작을 가지고 있어야만 한다. 예를 들어 input을 보자면, 많은 상태를 가지고 있다: 유효한지, 터치되었는지, 포커스되었는지, 더티상태인지 등등. 당신은 이러한 모든 상태가 시각적으로 표현되기를 원할 것이다. 에러나 경고를 표시하는 등 말이다. 우리는 Redux-form의 솔루션을 선택했다. 이 솔루션은 컴포넌트의 상태에 대한 메타 정보를 제공한다. 과연 redux form 컴포넌트를 어떻게 사용할까? 컴포넌트는 어떻게 각각 개별적인 input과 어떻게 Redux 스토어를 연결시킬까?(다음 주제에서 이를 연마할 것이다)

<Field name={keyName} component={MyCustomInput} {...this.props} />

이를 활성화하기 위해서 해야할 것은 단 한가지다. 위 구문처럼 컴포넌트를 <Field /> 컴포넌트로 name과 함께 감싸는 것이다. 그러면 당신이 원하는 대로 컴포넌트는 UI 상태들을 표시할 수 있게 된다. redux form의 <Field />를 생성하는 여러 방법들이 있으니 이 문서를 참조하라.

따라서 이제 UI의 각 상태를 다음과 같이 추가할 수 있다.

const {meta: {error, touched, dirty}} = this.props;
<div>
     <input
       disabled={isDisabled}
       type={type || "text"}
       placeholder={placeholder || 'Enter a value'}
       name={name}
     />
</div>
{error && <label className="error">{error}</label>}
{touched && <label className="touched">{touched}</label>}
{dirty && <label className="dirty">{dirty}</label>}

위에서 보듯이 <Field />를 사용하면 공짜로 UI 상태를 얻을 수 있다. 그리고 상태에 따라 알맞은 알림을 표시할 수 있게 된다.

# 데이터를 어디에 유지해야 하는가?

주요 질문 중 하나는 데이터를 어디에 유지해야 하는 가이고 이에 대해 대답할 필요가 있겠다. 그 대답을 위해서 먼저 걱정거리들을 찾아보자: 모든 폼 요소들은 초기값을 가지고 있어야만 하고, 폼 중 하나는 “new”라는 상태를 가질 때 비워져야 하거나, “edit”라는 상태를 위한 값을 가져야할 수도 있다. blur 혹은 keyup과 같은 어떠한 변화(혹은 관심을 가져야할 만한 어떠한 이벤트)에서도 값이 변경되어야 한다. 그래서, 어디에 둔단 말인가?

Redux-form의 솔루션은 Redux reducer다. 이 reducer들은 redux-form의 액션 디스패치를 듣고서는 Redux 내의 폼 상태를 관리한다. 폼들은 “form” key name 내에 마운트된다. 그리고 모든 폼은 고유한 이름을 가지고 있어서 이를 form[name]을 통해 접근할 수 있다.

아래의 코드를 보면 쉽게 이해할 수 있다.

import { createStore, combineReducers } from 'redux'
import { reducer as formReducer } from 'redux-form'

const reducers = {
  event,
  account,
  user,
  form: formReducer     
}
const reducer = combineReducers(reducers)
const store = createStore(reducer)

redux-form은 초기값과 현재값을 위한 셀렉터를 제공한다. 이것을 사용하는 것이 훌륭한 이유는 그것이 바로 redux 자체라는 것이다. 특정 커스텀 액션을 듣고서 데이터를 그에 맞게 조작하는 것이다. redux-form만의 어떤 액션이 아니고 당신이 만든 특정 액션에 대해서도 말이다. 그저 플러그인 함수만 사용하면 된다.

지금까지는 컴포넌트의 현재 상태를 표시하는 것에 대해 이야기하고, 데이터를 관리하는 위치에 대해서 이야기했다.

그러나 데이터 자체를 다루는 것은 어떤가?

# 유효성 검사는 어떻게 수행하는가?

모든 폼들에서 가장 주요한 도전과제는 유효성 검사다. 사용자 입력을 제출할 때(혹은 그에 준하는 어떤 브라우저 이벤트든 간에) 폼 컴포넌트와 그 값이 업데이트 하기 충분하지 않을 때 값에 대한 유효성 검사가 필요해진다. 대답해야할 질문의 이슈는 다음과 같다: 폼 컴포넌트의 값을 상태에 두고 유효성 검사가 통과하면 스토어로 전달해야하나? 아니면 스토어에 있는 모든 데이터에 대해 유효성 검사를 수행해야 하나?

Redux form은 모두에 대한 대답을 한다. 유효성 검사는 라이브러리의 에코시스템 중 한 부분이다. redux form을 이용해 생성한 폼에 validation method를 전달할 수 있다. 혹은 특정한 <Field />에 대해서 (우리만의 특정한 버전으로부터)유효성 검사를 수행할 수도 있다. 1가지 제약 사항만이 존재한다. 바로 error 오브젝트의 구조다. 다음과 같다. { field1: <string>, field2: <string> }. 3rd 파티 라이브러리를 사용하거나 직접 validation 메서드를 구현할 수 있다. 우리는 후자를 택해서 validator.js를 사용한다. 이 라이브러리는 거대한 양의 유효성검사를 커버하고, 특별한 검사를 위한 플러그인 기능을 지원한다. 그리고 가장 중요한 것은 redux form이 요구하는 error 오브젝트를 반환한다.

const validate = (data, props) => {

    const rules = {
        'ga-id': ['regex:/^UA-\\d{4,10}-\\d{1,4}$/'],
        'input': 'max:50 | required'
    };

    const messages = {
        'max.input': 'This is my max value text',
        'required.input': 'This is my required text'
        'regex.ga-id': 'My Google Analytics id format text'
    };

    const validator = new Validator(data, rules, messages);
    validator.passes();
    return validator.errors.all();
};

validator.js를 사용한 유효성 검사

위 예제를 통해 유효성 검사를 살펴보자. 몇몇 검사를 각각 필드(예. input)에 제공할 수 있고, 각각 에러에 대한 텍스트를 제공할 수 있다. 그리고 검사를 한 번 수행한 다음 error 오브젝트를 반환하면 된다.

# 폼의 상태와 그 변화는 어떻게 탐지하는가?

우리의 마지막으로 풀어야할 큰 항목은 폼의 현재 상태에 대한 탐지와 변화에 대한 추적이다. 예를 들어보자. 우리가 원하는 것은 원래 값으로부터 변했는지 여부다. 사용자가 다른 곳으로 이동하기 전에 저장하지 않은 변경사항이 있는지 알려주기 위한 것이다. Redux Form은 현재 폼 상태에 대한 완전한 시야를 제공한다. dirty, pristine 등등과 심지어 사용자에 의한 touched 여부까지 말이다(해당 요소에 포커싱을 준 적이 있는지). 그리고 redux 액션을 통해 상태 변화를 유발하는 것을 허용하는 외부 API도 제공한다. 예를들어 폼을 제출할 때나, 혹은 폼을 리셋할 때나 redux 액션을 사용해서 실제 제출 버튼을 누를 필요가 없다면 실제 제출 버튼 없이도 이를 수행할 수 있다.

결론

폼은 언제나 까다롭다. 모든 폼들은 어떠한 요구사항이 있고 가끔은 회피하기 위한 솔루션이 필요할때도 있다. 하지만 대부분의 시나리오는 redux-form에 의해 커버할 수 있다. 그리고 그게 안된다면 당신만의 솔루션을 redux form으로 마이그레이션할 수 있다. 우리에게는 훌륭하게 동작하고 있으므로, 당신에게도 그렇게 되었으면 좋겠다.

Bizzabo에서 함께 일합시다! 합류하세요!

원문보기

개발자들은 Higher Order Components(HOC), Stateless Functional Components를 적용하고 있고 그럴만한 이유가 있다: 그것들이 개발자들의 열망 중 하나인 코드 재사용을 보다 쉽게 만들어주기 때문이다.

HOC와 Functional Stateless Components에 대한 많은 글들이 있다. 몇몇은 소개글이고, 몇몇은 깊이 파고드는 관점에서 서술된 것이다. 나는 기존 컴포넌트를 리팩토링해서 재사용가능한 엘레멘트를 만드는 관점에서 이 글을 쓴다.

당신은 아마도 코드 재사용이 과대 평가되었다고 생각할 것이다. 혹은 그게 너무 어렵다고 생각할 것이다. 특히 웹과 모바일 간에 코드를 공유하는 관점에서 말이다. 그러나 여기 몇 가지 고려해볼만한 장점이 있다:

  • UX 일관성. 디바이스 사이에서 혹은 애플리케이션 사이에서
  • 교차 보정 업그레이드하기: 사용되고 있는 모든 컴포넌트를 향상시키고 업데이트하는 일들
  • 라우팅과 인증 규칙들을 재사용하기
  • 라이브러리 전환(예를 들어, 애플리케이션 상태관리에 MobX를 사용하고 있는 앱들을 Redux로 바꾸는 일)

나는 재사용을 달성하기 위한 HOC와 Functional Stateless Component에 중점을 둘 것이다. 당신은 ReactReact Native에 대해 익숙해야만 한다. Alexis Mangin이 그들의 차이점에 대해 설명한 좋은 을 썼다.

이 글에는 많은 양의 세부 사항이 있다. 나는 컴포넌트 리팩토링에 대해 점차적인 접근 단계로 설명할 것이다. 만약 이러한(HOC같은) 아이디어에 친숙하다면, 시간을 절약하기 위해서, 혹은 인내하기 힘들다면, The Payoff: Reusing the Components(최종 Github 저장소)로 점프하라. 재사용된 컴포넌트들을 이용해서 추가적인 애플리케이션을 만드는 것이 얼마나 쉬운 일인지 알게될 것이다.

Higher Order Components와 Stateless Functional Components란 무엇인가?

React 0.14에서 Stateless Functional Components가 도입되었다. 바로 컴포넌트를 렌더링하는 함수들이다. 문법이 더 간단하다. 클래스 정의나 컨스트럭터는 존재하지 않는다. 그 이름이 뜻하는 바와 같이 상태 관리도 없다(setState를 쓰지 않는다). 조금 뒤에서 튜터리얼의 후반부에 예제를 가지고 더 이야기하겠다.

Cory House가 좋은 소개글을 썼다.

Higher Order Components(HOC)는 새로운 컴포넌트를 만들어내는 함수다. 다른 컴포넌트(혹은 컴포넌트들)를 감싸고, 감싸진 컴포넌트를 캡슐화한다. 예를들면, 간단한 텍스트 박스를 상상해보자. 여기에 자동완성 기능을 추가하고 싶다. 그렇다면 HOC를 만들어서 이를 통해 텍스트박스를 감싸면 자동완성 텍스트 박스를 사용할 수 있다.

const AutocompleteTextBox = makeAutocomplete(TextBox);
export AutocompleteTextBox;

//…later

import {AutoCompleteTextBox} from ./somefile;

페이스북의 문서가 여기 있다. franleplat가 또한 상세한 내용의 을 썼다.

우리는 앞으로 HOC와 Stateless Function Components를 몇몇 지점에서 사용할 것이다.

샘플 애플리케이션

우리는 매우 간단한 애플리케이션으로 시작할 것이다. 단순한 서치 박스다. 쿼리를 입력하면 결과 목록을 얻을 수 있다. 우리 경우에는 이름으로 색상을 찾을 것이다.

화면이 하나뿐안 애플리케이션이다. 컴포넌트 재사용에 집중하기 위해 라우트나 여러 화면으로 구성된 애플리케이션을 사용하지는 않을 것이다.

두 번째로, 우리는 애플리케이션 한 쌍을 추가할 것이다(React와 React Native). 컴포넌트를 추출해서 재사용할 애플리케이션들이다.

Github 저장소 브랜치에 시작점의 애플리케이션이 있다(마지막 결과는 여기다). React(web), React Native(mobile)에 대한 전체 세부 사항을 README에 적어두었다. 그러나 여기에서 개요를 설명한다:

  • create-react-app을 통해 React 애플리케이션을 시작한다
  • React/web 애플리케이션에는 Material UI를 사용한다
  • react-native init을 통해 React Native 애플리케이션을 시작한다
  • 앱 상태 관리는 MobX를 이용한다. (Michel Weststrate, Mobx의 창시자가 훌륭한 튜토리얼을 이곳이곳에 두었다)

https://colors-search-box.firebaseapp.com/ 에서 웹 버전의 동작하는 데모를 확인할 수 있다. 각각의(web, 그리고 mobile) 스크린샷은 아래와 같다.

재사용을 위한 리팩토링

코드 재사용은 관점에 대한 것이다

코드 재사용의 기본은 간단한다. 메서드(혹은 클래스나 컴포넌트)를 코드베이스에서 추출한 다음, 포함된 값들을 파라미터로 바꾼다. 그리고 그것들을 다른 코드베이스에서 사용한다. 그러나 재사용된 요소의 이점은 썩 많지 않을 때가 많으며 공유된 코드를 유지하는데 많은 비용을 소모해야할 수도 있다.

그러나 나는 다음 지침들을 적용함으로써 지속적인 재사용을 달성했다. 관심사 분리, 단일 책임 원칙, 그리고 복사본 제거.

Separation of Concerns(SoC)와 Single Responsibility Principle(SRP)는 동전의 양면이다. 주요 아이디어는 주어진 코드는 반드시 하나의 주요 목적을 가져야 한다는 것이다. 하나의 목적만 있다면, Separation of Concerns는 제품에 자연스럽게 반영된다. 하나의 목적을 가진 요소는 아마도 두 가지 책임을 가진 영역으로 섞이지 않을 것이다.

많은 IDE와 개발 도구들은 복사된 코드들을 병합하는 자동화 기능을 제공한다. 그러나 유사한 디자인 사이에서 복사본 제거는 더 어렵다. 당신은 코드 블록을 재정렬해야하는 복사본들을 직접 “봐야”만 한다.

이 아이디어를 적용하는 것은 퍼즐 조각을 움직여서 조각들이 만나는 곳, 조각들이 드러내는 패턴이 무엇인지 찾는 것과 동일하다.

복사본을 찾으러 떠나보자.

복사본 보기

웹과 모바일 애플리케이션은 두 메인 컴포넌트가 있다. 웹 애플리케이션에서는 App.js

모바일 애플리케이션에서는 SearchView.js

구조 개요는 다음과 같다.

거의 동일하지만 React와 React Native간 플랫폼 차이점이 있다

두 컴포넌트는 유사한 구조를 지녔다. 이상적으로는 다음과 같이 생긴 컴포넌트를 공유할 수 있다.

우리의 목표: 공통의 공유된 컴포넌트 세트

유사-코드로는 다음과 같다.

유감스럽게도 두 애플리케이션 사이에는 매우 적은 코드만이 공통적이다. React가 사용하는 컴포넌트(이 경우에는 Material UI)는 React Native가 사용하는 컴포넌트들과 다르다. 그러나 관심사 분리를 통해 개념적인 중복을 제거할 수 있다. 그리고 컴포넌트를 단일 책임을 지도록 리팩토링한다.

관심사 분리와 단일 책임

App.jsSearchView.js는 모두 도메인 로직(우리의 애플리케이션 로직)과 플랫폼 구현, 라이브러리 통합이 섞여 있다. 이들을 고립시켜서 디자인을 향상시킬 수 있다.

  • UI 구현: ListItem, ListView를 분리하는 것 등
  • 상태 변경에 대한 UX: 결과를 보여주거나 업데이트하는 것으로부터 submitting을 분리 등
  • 컴포넌트들: search input, search results (list) 그리고 각각의 search result(list item) 등은 각각 컴포넌트로 분리해야만 한다

마지막으로, 이러한 많은 변화들이 아무 것도 망치지 않는 것을 보장하기 위한 자동화된 테스트를 통해 리팩토링이 완료된다. 이러한 간단한 “smoke” 테스트를 추가할 것이다. 이 Github 저장소/태그 에서 확인할 수 있다.

Stateless Function Components 추출

쉽고 분명한 것부터 리팩토링하자. React는 컴포넌트에 대한 것이므로 컴포넌트들을 분리하자. 읽기 쉬운 Stateless Functional Components를 사용할 것이다.

SearchInput.js를 다음과 같이 생성할 수 있다:

React의 정수는 UI/View 프레임워크고, 위 컴포넌트에서 그 정수를 볼 수 있다.

오직 2개의 임포트된 엘레멘트만 있다: React (JSX를 위한 요구사항) 그리고 Material UI의 TextField - MobX도 없고 MuiThemeProvider도 없다. 색상 등등도 없다.

이벤트 핸들링은 핸들러(파라미터로 주어진)에게 위임된다. 단, Enter 키를 누르는 것을 제외되었다. 이것은 input box의 구현 고려사항이고, 이 컴포넌트에 캡슐화되야만 한다. (예를 들어, 다른 UI 위젯 라이브러리는 enter 키를 누르면 서브밋하는 기능이 포함되어 있을 수도 있다)

리팩토링을 이어가자. SearchResults.js를 생성할 수 있다:

SearchInput.js와 유사하다. 이 Stateless Functional Components는 단순하고 2개의 임포트만 가지고 있다. 관심사 분리(그리고 SRP)에 따라, 이 컴포넌트는 ListItem이라는 개별 검색 결과를 위한 파라미터를 전달받는다.

ListItem을 감싸는 Higher Order Component를 생성할 수도 있다. 그러나 현재는 Stateless Functional Components를 사용할 것이다. 나중에 HOC를 사용할 것이다. (점차적으로, 우리는 SearchResults.js를 HOC로 리팩토링할 것이다.)

개별 검색 결과를 위해서, ColorListItem.js를 만들 것이다:

이제 App.js를 리팩토링할 필요가 있다.

Higher Order Components 추출

가독성을 위해서 App.jsSearchBox.js로 이름을 변경하겠다. 이 컴포넌트의 리팩토링에는 몇 가지 선택지가 있다.

  1. SearchBoxColorListItemSearchResults로 전달하게 하기(prop으로)
  2. index.jsColorListItemSearchBox에 전달하고, SearchResults로 전달하게 하기
  3. SearchBox를 Higher Order Component(HOC)로 변환하기

(1) 방법은 다음과 같다:

아무것도 잘못된 것이 없다. SearchInput.jsSearchResults.js를 추출하는 논리적인 결론이다. 그러나 SearchBoxColorListItem이 바인딩되어서 관심사 분리를 위반한다. (SearchResults의 재사용도 제한한다.)

(2) 관심사들을 분리해서 고쳐보자

(재사용성을 명확히 하려고 colors prop을 searchStore로 이름을 변경했다.)

그러나 사용처를 보면 ColorListItemindex.js에서 prop으로 전달하고 있음을 알 수 있다.

다음 코드와 비교해보자:

(3)의 경우, 즉 HOC를 사용했을 때의 index.js다. 사소한 차이지만 중요하다. ColorSearchBoxColorListItem을 포함하고 있으며, ColorSearchBox은 자신이 사용하고 있는 특정 search result 컴포넌트를 캡슐화한다.

(searchStore, Colors는 prop이다. 애플리케이션 내에서 하나의 인스턴스여야만 한다. 하지만 주어진 구성 요소의 인스턴스, 즉 ColorSearchBox가 여러 개 있을 수 있다.)

따라서, SearchBox.js를 HOC로 다음과 같이 만들 수 있다.

SearchBox.js가 이전 섹션(복사본 보기)의 유사코드와 닮아보인다는 것을 알아차릴 수 있다. 잠시 후에 더 정제해볼 것이다.

React Native 컴포넌트 리팩토링

모바일 애플리케이션과 추출된 컴포넌트들을 이전 패턴에 따라 다음과 같이 리팩토링할 수 있다. SearchInput을 추출하는 것과 같은 모든 세부사항을 살펴보지는 않을 것이다. 그러나 이 사항들은 READMEGithub 저장소 브랜치에 있다.

대신, 공통의 SearchBox를 리팩토링하는데 집중할 것이다. 이 컴포넌트는 web(React)와 mobile(React Native)에서 모두 사용할 것이다.

Web과 Mobile 양쪽에서 공유하는 컴포넌트 추출하기

명확히 하기 위해서 SearchInput.js, SearchResults.js, SearchBox.jsWebSearchInput.js, WebSearchResult.js, WebSearchBox.js로 개명했다.

(Web)SearchBox.js를 보자

2-10, 19, 20, 26, 27 번째 줄은 React에 특정된 것이다.

MuiThemeProvider는 Material UI components의 container고, 오직 Material UI에만 직접적인 의존성이 있다. 그러나 SearchInputSearchResult에도 묵시적인 의존성이 있다. 이러한 의존성을 SearchFrame 컴포넌트를 도입해서 분리시킬 수 있다. 이 컴포넌트는 MuiThemeProviderSearchInputSearchResults을 하위 컴포넌트로 갖고 캡슐화시킬 것이다. 그 다음엔 SearchBox HOC를 만들 수 있다. SearchBoxSearchFrame, SearchResults, SearchInput을 사용할 것이다.

새로운 SearchBox.js는 다음과 같다.

복사본 보기 섹션의 유사코드와 비슷해보인다.

WebSearchBox.js의 내용을 바꿀 차례다

WebSearchBox (26번째 줄) 은 SearchBox HOC를 사용한 결과다.

children은 특별한 React prop이다. 이 경우에는 WebSearchFrameWebSearchInputWebSearchResults을 포함하고 렌더링하도록 해준다. SearchBox에 의해 제공된 파라미터로 말이다. children prop에 대해 더 알고 싶다면 이곳을 확인하라.

또한 WebSearchResults를 HOC로 변경할 것이다. ListItem을 HOC 조합의 한 부분으로 캡슐화해야만 한다.

이제 재사용가능한 컴포넌트 세트를 가지게 되었다. (여기 Github 저장소와 브랜치가 있다. 주의, 몇몇 디렉토리는 명확성을 위해 이름을 바꿨다.)

결과: 컴포넌트 재사용

우리는 Github 저장소 검색 앱을 만들었다. (Github는 API key 없이 API를 사용하는 것을 허용한다. 이 튜토리얼에서 편리하게 쓰였듯이 말이다.)

초기설정과 같은 세부사항은 건너뛸 것이지만, 요약하자면 다음과 같다.

  • 웹 앱을 위해서는 create-react-app을 사용한다. 모바일 앱을 위해서는 react-native init을 사용한다
  • MobX, Material UI(웹앱용), qs(쿼리 스트링 인코딩) 등등을 추가한다. 더 자세한 내용은 package.json에 나와있다(, 모바일)

노력의 대부분은 새로운 검색 스토어를 작성하는 일이다. 이를 통해 컬러 대신에 Github 저장소들을 Git API를 통해 검색한다. 다음과 같이 github.js를 생성할 수 있다.

(유닛 테스트는 이곳에 있다)

단순함을 위해 몇몇 공통 파일을 복사할 것이다. GitHub 저장소에서는 파일을 복사할 때 약간의 편리함을 위해 webpack을 사용한다. 자바스크립트 프로젝트에서 파일/모듈을 공유하는 것은 보통 NPM이나 Bower를 이용한다. (돈을 지불하면 private module을 등록할 수 있다) 혹은 Git submodules를 사용할 수도 있다. 비록 어설프더라도 말이다. 우리는 모듈 배포가 아니라 컴포넌트 재사용에 집중하고 있기 때문에 그저 파일을 복사하는 조금 우아하지 못한 짓을 하는 것이다.

나머지는 쉽다. app.js를 삭제하고(App.test.js도) index.js의 내용을 다음과 같이 바꾼다.

이제 npm start를 실행하면 다음 화면을 볼 수 있다.

(https://github-repo-search-box.firebaseapp.com 에 가서 라이브 버전을 볼 수 있다)

React Native:Github 모바일 앱

github.jsMobileSearch*.js를 복사한 다음, GitHubMobileSearchBox.js를 생성한다.

그리고 index.ios.js의 내용을 다음과 같이 변경한다.

두 개의 파일만으로 새로운 모바일 앱이 만들어진다. react-native run-ios

리팩토링은 어려운 작업일 것이지만, 컴포넌트 재사용은 새로운 두 가지의 앱을 간단히 만들어낼 수 있다.

리뷰와 요약

우리 컴포넌트들에 대한 다이어그램을 살펴보자:

리팩토링 결과는 훌륭하다. 새로운 앱에서는 특정 도메인 로직에 집중할 수 있게 해준다. 단지 GitHub API 클라이언트와 저장소 결과를 렌더링하는 법만 정의했을 뿐이다. 나머지는 “무료”로 제공된 것이다.

게다가, 비동기 문제를 다룰 필요가 없다. 예를 들자면 github.js에서의 비동기 fetch 호출에 대해서는 알지도 못한다. 이는 리팩토링 방식의 놀라운 이점 중 하나며 Stateless Functional Components를 활용한 방법이다. 프라미스와 비동기 프로그래밍은 오직 필요한 곳, github.js에서만 발생할 뿐이다.

이러한 기술들을 몇 번 정도 적용해보면 컴포넌트를 추출하고 재사용하는 것이 더 쉬워질 것이다. 아마도 코딩이 패턴화되면 새로운 뷰의 시작지점에서부터 재사용 가능한 컴포넌트를 작성하게될지도 모른다.

또한 recompose와 같은 라이브러리를 살펴보면 HOC를 더 쉽게 작성할 수도 있다.

최종 GitHub 저장소를 살펴보고 당신만의 재사용가능한 컴포넌트에 대한 리팩토링 방법을 알려주세요.

다음: Microinterations and Animations in React. 이 게시물에 ♡를 눌러주시고 Medium혹은 twitter에서 저를 팔로잉 해주시기 바랍니다.

원문보기

새로운 언어의 문법들부터 JSX같은 커스텀 파싱까지, 자바스크립트 작성을 엄청나게 동적으로 만들어준 ES6와 바벨같은 것들에 감사한다. 나는 스프레드 오퍼레이터에 팬이 되었다. 이 3개의 점은 아마도 당신의 자바스크립트 태스크를 완료하는 방법을 바꿔놓을 것이다. 이 글의 다음 부분에서 내가 최고로 좋아하는 스프레드 오퍼레이터의 사용법을 나열할 것이다!

Apply없이 함수 호출하기

Function.prototype.apply를 호출할 때, 인자를 배열로 전달하기 때문에 인자들을 배열에 세팅해야 한다.

function doStuff (x, y, z) { }
var args = [0, 1, 2];

// Call the function, passing args
doStuff.apply(null, args);

스프레드 오퍼레이터를 쓰면 전부 함쳐서 apply를 쓰는 것을 피할 수 있다. 단순히 함수 호출 시에 배열 앞에 스프레드 오퍼레이터를 붙이기만 하면 된다.

doStuff(...args);

코드는 짧아지고, 말끔해지고, 쓸모없는 null을 넣어야할 필요도 없다! The code is shorter, cleaner, and no need to use a useless null!

배열 합치기

항상 배열을 합치는 다양한 방법들이 있었다. 그러나 스프레드 오퍼레이터는 배열을 합치는 새로운 방법을 제공한다.

arr1.push(...arr2) // arr2의 항목들을 뒤쪽에 추가한다
arr1.unshift(...arr2) // arr2의 항목들을 앞쪽에 추가한다

만약 두 배열을 함칠 때 아무 곳에서 요소를 추가하고 싶다면, 다음과 같이 하면 된다:

var arr1 = ['two', 'three'];
var arr2 = ['one', ...arr1, 'four', 'five'];

// ["one", "two", "three", "four", "five"]

다른 메서드들 보다 더 짧은 문법에 위치 제어 기능까지 추가된다!

배열 복사하기

배열을 복사하는 것은 빈번한 작업이다. 이전에는 Array.prototype.slice를 사용하곤 했다. 그러나 새로운 스프레드 오퍼레이트를 쓰면 배열을 다음과 같이 복사할 수 있다.

var arr = [1,2,3];
var arr2 = [...arr]; // like arr.slice()
arr2.push(4)

기억하라: 배열 안의 오브젝트는 여전히 레퍼런스다. 따라서 모든 것의 자체가 “카피”되는 것은 아니다.

arguments나 NodeList를 배열로 변환하기

배열을 복사하는 것과 비슷하게 Array.prototype.slice를 이용해서 NodeList와 arguments 객체들을 진짜 배열로 변환했었다. 그러나 이제 스프레드 오퍼레이터로 작업을 완료할 수 있다.

[...document.querySelectorAll('div')]

심지어 arguments를 함수 시그니처안에서 배열로 변환할 수도 있다:

var myFn = function(...args) {
// ...
}

Array.from을 사용할 수도 있다는 것을 잊지 말자!

Math 함수 사용하기

당연히 스프레드 오퍼레이터가 배열을 다른 arguments로 “펼치”기도 한다. 따라서 어떤 함수든 스프레드를 써서 arguments 수가 몇 개라도 받을 수 있는 함수들에 arguments를 전달할 수 있다.

let numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1

Math 오브젝트의 함수 세트들은 스프레드 오퍼레이터를 함수 인자로 사용하는 것에 대한 완벽한 예제다.

재미있는 디스트럭쳐링

디스트럭쳐링은 나의 많은 React 프로젝트에서 사용한 재미있는 연습이다. 뿐만아니라 Node.js 앱에서도 마찬가지다. 레스트 오퍼레이터과 디스트럭처링을 통해 정보를 추출해서 변수에 할당할 수 있다. 다음과 같이 말이다:

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }

나머지 프로퍼티들은 스프레드 오퍼레이터 뒤쪽의 변수에 할당된다!

ES6은 자바스크립트를 효율적으로 만들 뿐만 아니라 재미있게도 만든다. 모던 브라우저는 모두 ES6 문법을 지원하기 때문에 별로 이들을 사용해서 놀아본 적이 없다면 반드시 해보는 것이 좋을 것이다. 환경에 관계없이 더 실험하는 것을 선호한다면 내 포스트 중 Getting Started with ES6를 확인해보라. 어떤 경우든 스프레드 오퍼레이터는 아주 유용한 피처고, 당신은 반드시 이에 대해 알아야 한다!

원문보기

Styled-Components는 React와 React Native를 위한 라이브러리로 자바스크립트와 CSS를 혼합하여 컴포넌트 레벨의 스타일링을 사용할 수 있게 해준다.

Styled-Components에 익숙하지 않다면 웹사이트를 방문해서 살펴보라.

기본적인 Styled-Components의 컴포넌트 모양은 다음과 같다:

지난 몇 주간 두 개의 프로젝트에서 Styled-Components를 사용했으며, 발견된 몇 가지 이점과 패턴을 적어보고자 한다.

압축된 스타일

Styled-Components는 스타일 값으로 함수를 전달할 수 있기 때문에, 기존 CSS처럼 HTML 엘리먼트에 새 클래스를 추가하는 것이 아니라 prop 값을 기반으로 해서 스타일을 바꿀 수 있다.

결과적으로 코드량이 줄어든다. 처음에 CSS를 Styled-Components로 전환했을 때, 아주 드라마틱한 향상을 볼 수 있었다.

원본 CSS

Styled-Components로 변환

명확해진 JSX

당신이 나처럼 JSX를 <div><span>으로 흐트려뜨리는 사람이라면, 아마도 Styled-Components가 기본적으로 더 의미에 맞는 컴포넌트 계층 구조를 구성하게 한다는 사실을 알 수 있을 것이다.

클래스를 통해 스타일링한 원본 JSX

className이 사용되지 않은 Styled-Components로 변환된 JSX! 태그의 의미를 보라

난 이미 당신의 JSX가 두 번째 예제처럼 보일 것이라는 것을 확신한다 :P. 그게 아니라면, Styled-Components는 성공으로 가는 기본 통로로 당신을 이끌어 당신에게 큰 도움을 줄 수 있을것이다.

스타일 합성

이것은 Styled-Components에서 내가 가장 좋아하는 피처다. 한번 Styled-Components를 생성하고 나면 새로운 Styled-Components에 쉽게 합성할 수 있다.

왜냐면 Styled-Components에게 DOM 엘레멘트 뿐만 아니라 컴포넌트도 전달할 수 있기 때문이다.

Message를 가지고 합성된 두 새로운 컴포넌트, Success와 Danger

Prop 필터링

React 15.2.0부터 DOM 엘레멘트가 알지 못하는 prop을 전달하면 경고가 발생한다. <div foo="foo">와 같이 전달하면 “foo”가 알지못하는 속성이라는 경고 메시지가 나온다.

때로는 컴포넌트가 가능한 모든 DOM 속성을 허용하고 내부의 DOM 엘레멘트로 이를 전달해야하는 경우가 있다. 모든 DOM 속성을 수동으로 지정하여 이 작업을 수행하기는 어렵다. 따라서 이러한 컴포넌트는 props를 다음과 같이 펼쳐버린다: <div {...props}>

우리는 앞서 언급한 알지못하는 prop 경고를 피하기 위해 유효한 DOM 속성이 아닌 prop을 필터링하기 시작했다. 흥미롭게도 Styled-Components는 이미 내부적으로 이 작업을 수행하고 있는 잘 관리되는 라이브러리라서 이러한 필터링에 대한 항목을 업데이트할 필요가 없었다.

<span>과 같은 DOM 엘레멘트에 유효한 DOM 속성을 필터링하는 함수를 가지고 있었다

이런 필터링으로부터 자유로워졌다! 심지어 아무런 스타일도 필요하지 않은 경우에도!

이들은 우리가 경험한 패턴이나 이점의 일부에 지나지 않는다. 우리가 계속 사용한다면 시간이 갈 수록 더 많은 것들이 나타나리라 확신한다.

Styled-Components를 만든 Max Stoiber와 Glen Maddern에게 큰 감사를 표한다.