[번역] 리액트에서 피해야 할 안티패턴

2020. 7. 7. 10:33프론트엔드 개발/React

React와 관련한 글을 메일링서비스로 받아보던 중 이해하기 쉽고 clap 수가 높은 글을 발견해 번역해봤습니다. 원본 글에서 댓글을 통해 반박 내용이 있는데, 번역본에도 포함했습니다. 원본 글은 https://levelup.gitconnected.com/react-antipatterns-to-avoid-7134940f4f04 입니다.

Photo by Luca Bravo on Unsplash

리엑트(React)는 인터렉티브한 모던 웹앱을 만들기 위해 가장 많이 사용하는 프론트엔드 라이브러리입니다. 모바일 앱을 만들 때에도 쓸 수 있죠.

 

이번 글에서 우리는 리액트에서 피해야 할 안티패턴에 대해 알아볼 것입니다.

컴포넌트에서의 bind()와 함수들

우리가 리액트 class 컴포넌트에서 bind를 호출할 때, 우리의 props 안에서 반복적으로 부르지 않아야 합니다. 우리는 constructorbind를 이용할 수 있습니다. 이걸 이용하면 class 컴포넌트 메소드를 props로 전달할때, bind를 꼭 호출하지 않아도 됩니다.

 

아래 코드를 봅시다.

import React from "react";
export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: ""
    };
    this.updateValue = this.updateValue.bind(this);
  }
  updateValue(e) {
    this.setState({
      name: e.target.value
    });
  }
  render() {
    return (
      <>
        <form>
          <input onChange={this.updateValue} value={this.state.name} />
          {/* 아래는 안티패턴입니다. */}
          {/* <input onChange={this.updateValue.bind(this)} value={this.state.name} /> */}
        </form>
        <p>{this.state.name}</p>
      </>
    );
  }
}

(코드 내 주석은 이해를 돕기 위해 역자인 제가 추가했습니다.)

 

위 코드엔 input의 값(value)가 바뀌었을때 updateValue 메소드를 호출하는 input이 있습니다.

 

여기서

this.updateValue = this.updateValue.bind(this);

constructor 안에 이런 코드가 있기 때문에 이것은 항상 this의 값으로서 컴포넌트에 바인딩됩니다.

 

이제 어디에서도 이걸 반복할 필요가 없습니다. 또한 새로운 함수를 반환하는 bind를 호출하지 않았기 때문에 onChange 이벤트가 발생할때마다 새로운 함수를 매번 생성하지도 않을 것입니다.

 

앱의 규모가 커졌을 때, 그때 그때 메소드를 생성해내면 성능이 눈에 띄게 저하 될 것입니다. 아래 코드는 바로 이런 성능저하를 만드는 안티패턴입니다.(위 코드에서의 주석과 동일합니다.)

<input onChange={this.updateValue.bind(this)} value={this.state.name} />

key를 빼먹거나 index 값 쓰기

아무것도 아닌 것 같아 보일지라도, 리스트를 렌더링할 때 key prop은 굉장히 중요합니다. 유일한 key prop 값은 리액트가 리스트 아이템을 구분할 수 있게 해줍니다.

 

key prop 값은 변경 전의 트리(오리지날 트리)의 자식요소와 변경 후의 트리(결과 트리) 자식요소를 매칭시키기 위해서도 사용됩니다.

 

따라서 배열의 index값 또한 key값으로 사용해선 안 되며, 사용하게 될 경우 React가 잘못된 데이터로 렌더링하게 될 것입니다.

 

최선의 방법은 key값으로 유일한 ID를 부여하는 것입니다. 우리는 아래와 같이 유일한 ID를 사용함으로써 배열을 만들 수 있습니다.

import React from "react";
import { v4 as uuidv4 } from "uuid";
const arr = [
  {
    fruit: "apple",
    id: uuidv4()
  },
  {
    fruit: "orange",
    id: uuidv4()
  },
  {
    fruit: "grape",
    id: uuidv4()
  }
];
export default function App() {
  return (
    <div className="App">
      {arr.map(a => (
        <p key={arr.id}>{a.fruit}</p>
      ))}
    </div>
  );
}

위 코드에서 uuid 패키지를 사용해 key값으로 사용할 유일한 UUID를 만들어냈습니다. 이처럼 유일한 ID는 충돌할 염려가 거의 없습니다.

 

uuid 같은 패키지를 사용함으로써 ID는 유일하고 예측가능해지고, 우리는 각 아이템에 대해 새로운 ID를 생성할 방법을 고민할 필요가 없습니다.

컴포넌트 명은 대문자로 시작

컴포넌트 명은 반드시 대문자로 시작해야 합니다. 만약 우리가 아래 코드처럼 쓴다면 React는 에러를 뿜을 것입니다.

import React from "react";
const foo = () => <p>foo</p>;
export default function App() {
  return (
    <div className="App">
      <foo />
    </div>
  );
}

위 코드를 실행시키면 React는 <foo> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.(는 이 브라우저에서 인식할 수 없습니다. 만약 React 컴포넌트 렌더를 의도한 것이라면 그것의 첫 글자를 대문자로 해주세요.) 라는 에러를 뿜습니다.

 

그러므로 우리는 컴포넌트명을 반드시 대문자로 해야 할 것입니다.

import React from "react";
const Foo = () => <p>foo</p>;
export default function App() {
  return (
    <div className="App">
      <Foo />
    </div>
  );
}

그럼 우리는 우리가 의도한대로 화면에 'foo'가 보이는 것을 알 수 있습니다.

 

Photo by chatnarin pramnapan on Unsplash

모든 요소는 항상 닫아주기

모든 컴포넌트 태그는 항상 닫아주거나(클로징 태그), 태그 끝에 스스로 닫아주는 슬래쉬(/)를 써줘야 합니다. 가령 이렇게 한다고 해봅시다.

import React from "react";
const Foo = () => <p>foo</p>;
export default function App() {
  return (
    <div className="App">
      <Foo>
    </div>
  );
}

우리는 닫아주는 태그나 슬래쉬를 사용하지 않았으므로 문법 에러(Syntax Error)를 만나게 됩니다.

<Foo>

대신 이렇게 적어 줍시다.

import React from "react";
const Foo = () => <p>foo</p>;
export default function App() {
  return (
    <div className="App">
      <Foo />
    </div>
  );
}

<Foo></Foo> 이렇게 해도 정상적으로 작동합니다.

결론

React 컴포넌트를 쓸 때 이런 실수들을 하기 쉽습니다. 컴포넌트명을 대문자로 하고, 클로징 태그나 슬래쉬를 이용해 꼭 닫아줍시다.

 

또한 class 기반 컴포넌트를 사용할 때에 bind를 처음부터 호출함으로써 올바른 this가 무엇인지를 정해주기 위해 반복적으로 바인딩해주지 않아도 해주지 않아도 됩니다.


원문에 달린 댓글

  • Jay Braccini (24 May, 85 claps)
    나는 저들 중 어떤 것도 안티패턴이라 생각하지 않아요. 또 2020년에 아직도 binding 함수를 사용하는 이유가 무엇인가요?
    • John Au-Yeung (원문작성자)
      아닐 것입니다. 함수형 컴포넌트로 이동해야 할 시간이지만, 레거시 코드는 여전히 존재하죠.
  • Kristian Ulvund (23 May, 11 claps)
    엄밀하게 말해서, 컴포넌트를 소문자로 선언하는 것은 안티패턴이 아닙니다. 그냥 잘못된 것이고 앱의 충돌때문에 에러가 날 것입니다.
    • John Au-Yeung (원문작성자)
      그게 더 낫겠네요.
  • Dylan du Preez (22 May, 8 claps)
    key prop의 언급에 있어선 좋은 글이네요. 리액트 문서는 키는 예측가능해야 한다고 명시되어 있으므로 uuid를 key로 사용한다는 것에는 동의하지 않습니다. uuid를 할당하려면 좀 더 주의를 기울여야 합니다. 만약 클래스 컴포넌트의 렌더 메소드에 uuid를 사용한다면, 리스트 아이템의 매 렌더링마다 새로운 uuid가 할당될 것입니다. 이것은 매번 전체 리스트가 불필요하게 리렌더링하게 됩니다. 여기 적힌 예시는 알맞게 되어있지만 지적할만 하네요.
    • Florian Wege (22 May)
      맞아요, 모든 렌더링 싸이클에서 요소의 독립성을 유지하는 것은, key의 본래 목적을 무색하게 만드는 것입니다.
    • John Au-Yeung (원문작성자)
      감사합니다. 가장 좋은 key 값은 변하지 않는 unique ID이어야 하겠네요. 그렇지 않다면 이슈를 만날 것이고요.
  • Carlos Azuaje (May 28, 2 claps)
    고작 랜덤 id를 만들기 위해 uuid를 쓰라고 조언하는 건 이제 그만....
  • Jonas Winzen (May 26, 5 claps)
    render 안에서 바인딩 메소드를 쓰는 것은 안티패턴입니다. 그런데 handler를 쓰는 것도 똑같아요.
    다른 케이스들은 안티 패턴이 아니고 JavaScript와 JSX같은 확장언어를 정확하게 쓰지 않은 Syntax error의 사례라고 할 수 있겠네요. 이것들은 객관적으로 틀린거고 구동은 커녕 컴파일조차 되지 않을 것입니다.
    반면 안티패턴은 작동은 하나 단점을 가진 것들을 말합니다.(꼭 명백하게 단점이 아닐 수 있고, 다른 접근이 가능한 것과 비교해서 단 점일 수 있다는 것입니다.)
    예를 들어 공기 매트리스를 입으로 팽창시키는 것과 펌프/압축기로 팽창시키는 것은 모두 패턴입니다. 둘다 가능하지만 더 좋은 방법이 뭔지 판단할 수 있죠. 압축기 대신 입으로 자동차 타이어를 팽창시키는 것은 불가능합니다. 따라서 이런건 안티 패턴이라고 할 수 없습니다.
    • John Au-Yeung (원문작성자)
      클로징 태그에 대해선 그게 맞을 것입니다. 그러나 다른 사람의 모든 버전이 에러를 뿜을 거라 생각하진 않아요.
  • Mariano De Simone(May 28)
    클래스 컴포넌트 좀 그만 써.

(번역자 의견입니다.)

 

처음 읽었을때도 사실 안티패턴이라고 할만한가 싶은 내용이었는데, 실제로 댓글로도 관련 지적이 많았습니다. 또한 uuid를 key값을 주기 위해 사용하는 것도 딱히 좋은 방법은 아니라고 생각합니다.

 

bind의 경우에도 사실 요새 함수형 컴포넌트가 대세가 되어서 큰 의미있는 지적은 아니라고 생각합니다.

 

번역했던 것이 아까워서, 반박 댓글들과 함께 글 올려봅니다. 오히려 반박 댓글에 도움이 되는 내용이 많은 것도 같습니다. 개발 및 리액트의 이해에 도움이 되었으면 좋겠습니다.

 

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

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

 

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