[번역] React Clean Code를 위한 팁

2021. 3. 15. 21:25프론트엔드 개발/React

React와 관련한 글을 메일링서비스로 받아보던 중 이해하기 쉽고 clap 수가 1300으로 높은 글이 있어 원작자의 허락을 구해 번역해봤습니다. 원본 글에서 댓글을 여러 의견이 있었는데 이 번역글에서도 포함하여 일부 번역해봤습니다. 원본 글은 https://betterprogramming.pub/8-ways-to-write-clean-react-code-610c502ccf39 입니다.

React Clean Code

더 클린하고 더 나은 코드를 작성하는 8가지의 간단한 방법

 

Photo by Candid on Unsplash.

 

"클린코드"는 그저 동작만 하는 코드 그 이상을 의미합니다. 클린코드는 읽기 쉽고 이해하기 쉬우며, 깔끔하게 구성되어 있습니다. 이번 글을 통해 리액트 코드를 보다 "클린"하게 만드는 8가지 방법에 대해 알아보겠습니다.

 

그전에 이야기하고 싶은 것은 아래 방식들은 정확히는 "제안" 이라는 것입니다. 만약 이것들에 대해 동의하지 않는다고 해도 좋습니다. 하지만 이것들은 제가 리액트 코드를 짜면서 큰 도움이 되었던 방식입니다.

 

한번 살펴봅시다.


1. 단일 조건인 조건부 렌더링에는 && 를 쓰자

만약 어떤 값이 true 일 때 렌더링하고 false일 때는 렌더하지 않는 식으로, 조건에 따라 렌더링을 하거나 하지 않는 경우가 있습니다. 이때는 삼항연산자를 사용하지 말고 대신 &&연산자를 활용하는 것이 좋습니다.

 

나쁜 예시

import React, { useState } from 'react'

export const ConditionalRenderingWhenTrueBad = () => {
  const [showConditionalText, setShowConditionalText] = useState(false)

  const handleClick = () =>
    setShowConditionalText(showConditionalText => !showConditionalText)

  return (
    <div>
      <button onClick={handleClick}>Toggle the text</button>
	  {/* (역주) 삼항연산자를 사용하면 null 같이 불필요한 코드가 늘어납니다. */} 
      {showConditionalText ? <p>The condition must be true!</p> : null}
    </div>
  )
}

좋은 예시

import React, { useState } from 'react'

export const ConditionalRenderingWhenTrueGood = () => {
  const [showConditionalText, setShowConditionalText] = useState(false)

  const handleClick = () =>
    setShowConditionalText(showConditionalText => !showConditionalText)

  return (
    <div>
      <button onClick={handleClick}>Toggle the text</button>
	  {/* (역주) 삼항연산자를 활용하면 조건별 렌더링을 더 가시적으로 표현할 수 있습니다. */}
      {showConditionalText && <p>The condition must be true!</p>}
    </div>
  )
}

2. 다중 조건인 조건부 렌더링에서는 삼항연산자를 쓰자

만약 어떤 값이 true 일 때 A를 렌더링하고 false일 때는 B를 렌더하지 식으로 조건에 따라 서로 다른 결과를 렌더링해야 하는 경우가 있습니다. 이때는 삼항연산자를 쓰는 것이 좋습니다.

 

나쁜 예시

import React, { useState } from 'react'

export const ConditionalRenderingBad = () => {
  const [showConditionOneText, setShowConditionOneText] = useState(false)

  const handleClick = () =>
    setShowConditionOneText(showConditionOneText => !showConditionOneText)

  return (
    <div>
      <button onClick={handleClick}>Toggle the text</button>
	{/* (역주) 이 경우 && 연산자를 활용하면 불필요하게 코드가 늘어납니다. */}
      {showConditionOneText && <p>The condition must be true!</p>}
      {!showConditionOneText && <p>The condition must be false!</p>}
    </div>
  )
}

좋은 예시

import React, { useState } from 'react'

export const ConditionalRenderingGood = () => {
  const [showConditionOneText, setShowConditionOneText] = useState(false)

  const handleClick = () =>
    setShowConditionOneText(showConditionOneText => !showConditionOneText)

  return (
    <div>
      <button onClick={handleClick}>Toggle the text</button>
	  {/* (역주) 삼항연산자를 활용하면 조건별 렌더링을 더 가시적으로 표현할 수 있습니다. */}
	  {showConditionOneText ? (
        <p>The condition must be true!</p>
        ) : (
        <p>The condition must be false!</p>
        )}
    </div>
  )
}

3. Boolean값을 Props로 넘길 때는 true를 생략하자

따로 값을 할당하지 않아도 prop명만으로 컴포넌트에 참으로 평가되는 값을 제공할 수 있는 경우가 있습니다. 이런 경우에 해당 prop값으로 true 를 굳이 명시할 필요가 없습니다. 가령 myTruthProp={true} 이런 식으로 작성할 필요는 없다는 뜻입니다.

 

나쁜 예시

import React from 'react'

const HungryMessage = ({ isHungry }) => (
  <span>{isHungry ? 'I am hungry' : 'I am full'}</span>
)

export const BooleanPropBad = () => (
  <div>
    <span>
      <b>This person is hungry: </b>
    </span>
	{/* (역주) isHungry의 값으로 굳이 true를 명시 */} 
    <HungryMessage isHungry={true} />
    <br />
    <span>
      <b>This person is full: </b>
    </span>
    <HungryMessage isHungry={false} />
  </div>
)

좋은 예시

import React from 'react'

const HungryMessage = ({ isHungry }) => (
  <span>{isHungry ? 'I am hungry' : 'I am full'}</span>
)

export const BooleanPropGood = () => (
  <div>
    <span>
      <b>This person is hungry: </b>
    </span>
	{/* (역주) isHungry의 값으로 굳이 true를 명시하지 않아도 됩니다. */}
    <HungryMessage isHungry />
    <br />
    <span>
      <b>This person is full: </b>
    </span>
    <HungryMessage isHungry={false} />
  </div>
)

4. 문자열 값을 Props로 넘길 때는 쌍따옴표를 이용하자

문자열 Prop값은 별도의 중괄호({}, curly brace)나 백틱(``, backticks)없이 그저 쌍따옴표만을 통해서도 전달할 수 있습니다.

 

나쁜 예시

import React from 'react'

const Greeting = ({ personName }) => <p>Hi, {personName}!</p>

export const StringPropValuesBad = () => (
  <div>
	{/* (역주) 문자열 prop값을 중괄호, 백틱, 쌍따옴표, 홑따옴표로 감싸 전달한 사례 */}
    <Greeting personName={"John"} />
    <Greeting personName={'Matt'} />
    <Greeting personName={`Paul`} />
  </div>
)

좋은 예시

import React from 'react'

const Greeting = ({ personName }) => <p>Hi, {personName}!</p>

export const StringPropValuesGood = () => (
  <div>
  	{/* (역주) 문자열 prop값은 그저 쌍따옴표만으로도 충분히 전달할 수 있습니다. */}
    <Greeting personName="John" />
    <Greeting personName="Matt" />
    <Greeting personName="Paul" />
  </div>
)

5. 인자가 Event 객체 뿐인 이벤트 핸들러 함수는 함수명만 입력하자.

만약 이벤트 핸들러가 오직 Event 객체 하나만을 인자로 받는다면, 그냥 이벤트 핸들러로 함수명만을 입력하면 됩니다. 즉, onChange={e => handleChange(e)} 이라고 쓸 필요없이 그냥 onChange={handleChange}라고 쓰면 된다는 뜻입니다.

 

나쁜 예시

import React, { useState } from 'react'

export const UnnecessaryAnonymousFunctionsBad = () => {
  const [inputValue, setInputValue] = useState('')

  const handleChange = e => {
    setInputValue(e.target.value)
  }

  return (
    <>
      <label htmlFor="name">Name: </label>
      {/* (역주) Event 객체 하나만 인자로 받는데 인자를 전달하는 함수형태로 작성 */}
      <input id="name" value={inputValue} onChange={e => handleChange(e)} />
    </>
  )
}

좋은 예시

import React, { useState } from 'react'

export const UnnecessaryAnonymousFunctionsGood = () => {
  const [inputValue, setInputValue] = useState('')

  const handleChange = e => {
    setInputValue(e.target.value)
  }

  return (
    <>
      <label htmlFor="name">Name: </label>
      {/* (역주) 그저 Event 객체 하나만 인자로 받는 함수는 함수명만을 입력 */}
      <input id="name" value={inputValue} onChange={handleChange} />
    </>
  )
}

6. 별도의 props가 없는 컴포넌트 전달할 때는 컴포넌트명만을 입력하자

어떤 컴포넌트(B)에 prop으로 또 다른 컴포넌트(A)를 전달하는 경우에, 전달되는 컴포넌트(A)가 아무 props를 받지 않는다면 굳이 함수로 감쌀 필요없이 컴포넌트명만을 전달해도 좋습니다.

 

나쁜 사례

import React from 'react'

const CircleIcon = () => (
  <svg height="100" width="100">
    <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
  </svg>
)

const ComponentThatAcceptsAnIcon = ({ IconComponent }) => (
  <div>
    <p>Below is the icon component prop I was given:</p>
    <IconComponent />
  </div>
)

export const UnnecessaryAnonymousFunctionComponentsBad = () => (
  {/* (역주) CircleIcon은 아무런 인자를 받지 않는데도 함수로 감싸서 전달 */}
  <ComponentThatAcceptsAnIcon IconComponent={() => <CircleIcon />} />
)

좋은 사례

import React from 'react'

const CircleIcon = () => (
  <svg height="100" width="100">
    <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
  </svg>
)

const ComponentThatAcceptsAnIcon = ({ IconComponent }) => (
  <div>
    <p>Below is the icon component prop I was given:</p>
    <IconComponent />
  </div>
)

export const UnnecessaryAnonymousFunctionComponentsGood = () => (
  {/* (역주) 아무런 인자를 받지 않는 CircleIcon를 함수로 싸지 않고 컴포넌트명으로 전달 */}
  <ComponentThatAcceptsAnIcon IconComponent={CircleIcon} />
)

7. Undefined Props

undefined props는 제외됩니다. 만약 어떤 props가 undefined로 제공되어도 컴포넌트 작동에 문제가 없다면, props값으로 undefined를 전달하는 것에 대한 대비책을 걱정할 필요는 없습니다.

 

나쁜 예시

import React from 'react'

const ButtonOne = ({ handleClick }) => (
  <button onClick={handleClick || undefined}>Click me</button>
)

const ButtonTwo = ({ handleClick }) => {
  const noop = () => {}

  return <button onClick={handleClick || noop}>Click me</button>
}

export const UndefinedPropsBad = () => (
  <div>
    <ButtonOne />
    <ButtonOne handleClick={() => alert('Clicked!')} />
    <ButtonTwo />
    <ButtonTwo handleClick={() => alert('Clicked!')} />
  </div>
)

좋은 예시

import React from 'react'

const ButtonOne = ({ handleClick }) => (
  <button onClick={handleClick}>Click me</button>
)

export const UndefinedPropsGood = () => (
  <div>
    <ButtonOne />
    <ButtonOne handleClick={() => alert('Clicked!')} />
  </div>
)

8. 이전 상태에 의존하는 상태를 갱신할 때는 updater 함수를 전달하자

만약 새로운 상태가 이전의 상태값에 의존한다면, 이전 state값을 이용한 함수(updater 함수)를 전달해야 합니다. React 상태 갱신은 일괄적으로 이뤄지므로 이런 식으로 갱신하지 않으면 예상치 못한 결과가 나올 수 있습니다.

(역주) 아래 예시를 직접 구현해보고 Toggle button state 2 times 버튼을 눌러본다면 나쁜 예시에서 의도한대로 상태갱신이 이뤄지지 않음을 알 수 있을 것입니다.
- 관련 react 공식문서 https://ko.reactjs.org/docs/faq-state.html

 

나쁜 예시

import React, { useState } from 'react'

export const PreviousStateBad = () => {
  const [isDisabled, setIsDisabled] = useState(false)
  
  // (역주) 이전 값에 의존하는 상태갱신 함수에 갱신결과값만을 전달하면
  const toggleButton = () => setIsDisabled(!isDisabled)
  
  // (역주) 이 함수의 결과가 정상적으로 작동하지 않음을 알 수 있습니다.
  const toggleButton2Times = () => {
    for (let i = 0; i < 2; i++) {
      toggleButton()
    }
  }

  return (
    <div>
      <button disabled={isDisabled}>
        I'm {isDisabled ? 'disabled' : 'enabled'}
      </button>
      <button onClick={toggleButton}>Toggle button state</button>
      <button onClick={toggleButton2Times}>Toggle button state 2 times</button>
    </div>
  )
}

좋은 예시

import React, { useState } from 'react'

export const PreviousStateGood = () => {
  const [isDisabled, setIsDisabled] = useState(false)

  // (역주) 상태갱신 함수의 인자로 이전 값을 인자로 하는 updater 함수를 전달하면
  const toggleButton = () => setIsDisabled(isDisabled => !isDisabled)

  // (역주) 아래 함수가 의도한대로 동작함을 알 수 있습니다.
  const toggleButton2Times = () => {
    for (let i = 0; i < 2; i++) {
      toggleButton()
    }
  }

  return (
    <div>
      <button disabled={isDisabled}>
        I'm {isDisabled ? 'disabled' : 'enabled'}
      </button>
      <button onClick={toggleButton}>Toggle button state</button>
      <button onClick={toggleButton2Times}>Toggle button state 2 times</button>
    </div>
  )
}

Honorable Mentions 언급하진 않았지만 중요한 것들

아래 내용은 React의 필수법칙은 아니지만 클린코드를 짜는 좋은 방법이 될 것입니다. 해당 문제에 대해서는 자바스크립트 뿐만 아니라 어떤 프로그래밍 언어에서든지 말입니다.

  • 복잡한 로직을 명확한 이름의 함수로 뽑아내라.
  • 매직넘버(?)는 상수로 뽑아내라.
  • 명확한 이름의 변수를 사용해라

즐코하세염!


댓글 요약

원문 포스트에 달린 댓글들 중 claps 수가 높거나 유익한 이야기가 오갔던 것을 간단하게 요약해보았습니다.

  1. 8번의 특히 도움이 되었다는 의견이 많았습니다.
  2. 2번에 대해서 렌더 내부에 로직을 포함하는 것 자체에 대한 반대의견이 있었습니다. 분기렌더 로직은 렌더 가장 처음이나 외부로 빼내는 것이 가독성에 좋다는 의견이었습니다. (Michael Nagy, 5 claps)
    1. (역주) 대댓글에서 단순히 개행을 수정하는 것을 의미하는지 물어보는 내용이 있었는데 그것보다는 위에 쓴 것이 본래의 취지같습니다.
  3. 3번에서 isHungry에 대한 기본값을 설정하는 방법이 있다는 의견이 있었습니다(Julian Perl, 15 claps)
  4. 1번에서 JavaScript에서 truthy, falsy가 모호하기 때문에 && 키워드를 쓸 때 !!를 쓰는 것을 추천하는 의견이 있었습니다.(Cam, 21 claps)
    1. (역주) 가령 다음과 같이 작성하면 됩니다. {!!showContitionalText && <p>The condition must be true!</p>}
  5. 3번에서 true와 같은 Boolean 값을 생략하기보다 개발자의 의도를 명확하게 하기 위해 명시적으로 true, false 값을 입력해주는 것이 좋다는 의견이 있었습니다(Mourad Bougarne, 15 claps)

(번역자 의견입니다.)

 

문맥에 맞게 의역을 많이 했으나 혹시라도 이해가 안되셨다면 댓글로 질문을 주시거나 원문을 참고 부탁드립니다.

 

React 코드를 작성하면서 무심코 넘어가는 부분을 짚고 넘어가는 글이었습니다. 또한 특히 8번의 경우 updater 함수를 전달해야 하는 것이 기본 스펙 문서에도 작성되어 있음에도 불구하고 간과하기 쉬운 부분이었습니다. 본문 뿐 아니라 댓글 속 이야기에도 좋은 이야기가 오간 것 같아 번역해 보았습니다. 

 

5번의 경우에 handleChange가 바로 위에서 정의되어 있지만, 혹시라도 스토어의 메소드를 호출하는 경우라면 해당 메소드가 화살표함수로 짜여있지 않거나, 혹은 handleChange의 bind가 제대로 처리되어 있지 않으면 this를 제대로 바인딩하지 못해 의도하지 않은 결과를 낼 수 있지 않나 생각했습니다. 그러나 본문은 물론 요즘은 대부분 함수형 컴포넌트로 개발하고 있고 화살표 함수가 대중화된만큼 과한 걱정이라 생각이 드네요. 이후에 관련해 bind와 this에 대해 글을 써보려고 합니다.


번역 오류나 지적하실 내용, 질문이 있으시면 언제든지 댓글로 부탁드리며,

글이 도움이 되셨다면 아래 좋아요❤와 댓글💬 부탁드립니다.

 

피드백은 항상 제게 큰 도움이 됩니다😄

감사합니다.