Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[공유] 리액트 안티패턴 #12

Open
jaryapp opened this issue Nov 29, 2021 · 0 comments
Open

[공유] 리액트 안티패턴 #12

jaryapp opened this issue Nov 29, 2021 · 0 comments
Assignees
Labels
docs 문서

Comments

@jaryapp
Copy link
Contributor

jaryapp commented Nov 29, 2021

번역자:@vcho1958 (애드워드)

React Anti Patterns를 번역한 글입니다

React Anti Patterns

Anti-patterns는 소프트웨어 개발과정에서 생기는 나쁜 습관으로 간주되는 코딩 패턴들입니다.

형식화되어졌고 범용적으로 좋은 개발 습관으로 간주되는 일반적인 접근법인 design patterns와는 반대로 anti-patterns 들은 바람직하지 않습니다.

출처: StackOverflow

우리는 FusemachinesAjay라는 분에게 React의 Anti-Pattern에 대한 지식을 나누는 시간을 가졌습니다. JS 개발자들이 일반 적으로 행하기 쉬운 몇몇 anti-pattern들에 대한 통찰들을 나누는 좋은 시간이었습니다. 그 시간을 통해 얻을 수 있었던 중요한 사항들과 설명들이 아래 링크에 잘 나와있습니다.

deckAnti-patterns are certain patterns in software development that are considered bad programming practices. The same…slides.com

렌더링 도중 반복문에서 인덱스를 key로 사용하는 경우

비슷한 컴포넌트들을 렌더링하는 동안 key는 React가 어떤 item들이 제거되고 추가되고 변화되는 지 식별할 수 있도록 돕습니다. Key들은 안정적으로 element들을 식별할 수 있도록 배열안의 element들에게 주어져야만 합니다. 대부분의 JS 개발자들은 이러한 상황에서 anti-pattern에 해당하는 배열의 index를 key로 사용하는 경향이 있습니다.

아래 인용은 https://reactjs.org 공식문서에서 발췌했습니다.

항목의 순서가 바뀔 수 있는 경우 key에 인덱스를 사용하는 것은 권장하지 않습니다. 이로 인해 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있습니다. 렌더링 한 항목에 대한 안정적인 ID가 없다면 최후의 수단으로 항목의 인덱스를 key로 사용할 수 있습니다.

아래 블로그에는 배열의 index를 key로 사용하는 것이 왜 anti-pattern이 될 수있는 지에 대한 흥미로운 설명이 있습니다.

Why using an index as Key in React is probably a bad idea?Let’s say you have a list of items and you want to show them as ul, li elements in your React app. When the user clicks…medium.com

Solution

Key들은 인덱스(여기서는 데이터의 primary key나 _id값을 의미하는 것 같습니다.), 임의의 무작위 수나 타임스탬프처럼 안정적이며 유일하고 예측가능해야 합니다.

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li key={number.toString()}>
    {number}
  </li>
);

컴포넌트 내부에서의 상수 배치

우리는 상수들을 정의할 때 컴포넌트의 어디에 배치할 것인지 결정해야합니다. 대부분의 개발자들은 React의 render 메소드안에 상수 객체 리터럴들과 배열의 내부에 정의하는 경향이 있습니다. 이런 접근들은 처리량이 많은 컴포넌트 들에소 지연을 유발하고 성능에 영향을 줄 수 있습니다.

class App extends Component {
  render() {
    const styles = { width: "400px", height: "400px", position:  "relative" };
}
}

Solution

이미지나 스타일을 위한 상수들을 정의할 때 컴포넌트 외부에 정의하는 것이 좋습니다.

const styles = { width: "400px", height: "400px", position: "relative" };class MyComponent extends React.component {}

또는 constructor에서 정의하는 방법도 있습니다.

class MyComponent extends React.component {
  constructor(){
    this.styles = { width: "400px", height: "400px", position:
  }
}

props로부터 유래된 state관리

클래스 기반 컴포넌트에서 componentWillReceiveProps(to be deprecated) 와 getDerivedStateFromProps 메소드들은 props가 변화했을 때 state를 업데이트할 수 있는 방법을 제공합니다.

React 공식 문서에 따르면:

getDerivedStateFromPropscomponentWillReceiveProps 가 props가 변경 됐을 때에만 호출된다는 건 흔한 오해입니다. 이 라이프사이클들은 부모 컴포넌트들이 재렌더링될 때마다 호출되며 props들이 이전 상태와 다르지 않더라도 호출됩니다. 이것으로 인해 이 라이프사이클들 내부에서 state를 오버라이드하는 것은 state 업데이트들이 유실되는 결과를 초래할 수 있으므로 절대 안전하지 않습니다.

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  componentWillReceiveProps(nextProps) {
    // This will erase any local state updates!
    // Do not do this.
    this.setState({ email: nextProps.email });
  }
}

위의 예시에서 state들은 받은 props를 기반으로 업데이트됩니다. 그러나 만약 이 컴포넌트의 부모가 재 렌더링될 경우, 어떤 state 업데이트들은 유실될 수 있습니다.

Solution

  • redux와 같은 근원 저장소를 관리하는 상태관리 라이브러리를 사용
  • 데이터는 하향으로 액션은 상향으로 접근하는 방법이 단일방향흐름을 강조하는 것으로 사용될 수 있다. 이 접근은 데이터를 애플리케이션의 한 부분으로부터 그것의 자식컴포넌트로 하향 전달하는 우선순위를 정합니다. 그 자식컴포넌트는 데이터에 대해 읽기전용 접근은 가능하지만 직접적으로 수정할 수는 없습니다. 대신에 그 자식 컴포넌트는 부모 컴포넌트의 데이터를 업데이트할 수 있는 액션이나 콜백들에 접근할 수 있습니다.
  • 주석: 아래 코드는 2번째 해결방안에 대한 예시입니다. https://ko.reactjs.org/docs/lifting-state-up.html 에서 끌어온 예제인데 이것처럼 state는 부모만 쓰고 자식들은 변화시킬 수 있는 함수(onChange={this.handleChange})를 전달 받아서 사용하는 것을 설명하는 것 같습니다. 예시가 없어서 4주차에 3주차 마무리 짓느라 공부했던 거 되새김해봤습니다. ㅎㅎ
class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

promise를 사용하는 anti-pattern

Developers working with promisesasync await패턴을 사용하는 개발자들은 애플리케이션에서 혼란스러운 blogs(패턴?)을 도입하는 흔한 실수를 합니다.

loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
                    DoSomethingOnThem(something, another);
    });
});

Solution

이런 요소들은 코드를 리팩토링하는 것을 어렵게 만들고 promise를 네스팅하는 것을 올바르게 다루지 못하면 버그들을 유발할 수 있습니다. 명확한 해경방법은 promise.all을 사용하는 것 입니다.

promise.all([loadSomething(), loadAnotherThing()])
 .spread(function(something, another) {
 doSomethingOnThem(something, another);
});

async await패턴에서도 마찬가지입니다.

const async myFunction(){
  const res1 = await dataFromApi()
  const res2 = await dataFromAnotherApi()  
  doSomething(res1) //blocked by dataFromAnotherApi which is wrong
}

함수 async await패턴은 응답이resolve될 때까지 코드의 동작을 막습니다. 그래서, 만약 변수가 promise가 resolve되는 것에 의존하지 않는다면 await을 사용하는 것은 위의 예시처럼 anti-pattern로 간주할 수 있습니다.

setState 사용하기

setState는 React의 매우 간편하고 직설적인 콘셉트지만 개발자들은 setState를 사용할 때 종종 몇가지 뉘앙스들을 간과합니다.

아래의 예시가 바로 그것입니다.

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      counter: 350
    };
  }updateCounter() {    // this line will not work
    this.state.counter = this.state.counter + this.props.increment;
 
    // this will not work as intended
    this.setState({
      counter: this.state.counter + this.props.increment; 
    });
    
  }
  ...
}

setState 메소드는 비동기적으로 동작합니다. 따라서 우리는 위의 예시처럼 setState안에서 state안의 특정 value의 연산에 해당 value를 사용해서는 안됩니다. 해당 이슈를 피하기 위해 우리는 다음 예시처럼 사용해야합니다.

this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

prevStatesetState콜백함수에 전달되는 매개변수의 이름이며 setState가 동작하기 전의 state의 값들을 사용할 수 있도록 유지하고있습니다.

useEffect훅에서의 비동기 함수

비동기 api를 useEffect훅을 사용해서 불러오는 것은 중요합니다. 그래서 우리는 아래 예시처럼 코드를 작성하는 성향이 있습니다.

useEffect(async () => {
    try {
        const response = await fetch(http://hn.algolia.com/api/v1');
        setData(response)
    } catch (e) {
        console.error(e);
    }
}, []);

MDN에 따르면 비동기 함수 선언은 AsyncFunction객체를 반환하는 비동기 함수를 정의하는 것입니다. 하지만 useEffect 훅은 아무것도 반환하지 않거나 한 가지 clean-up함수(구독해제 등)를 반환해야합니다. 따라서 다음 오류메세지를 볼 수 있습니다.

img

img

올바른 접근 방법은 다음과 같습니다.

useEffect(() => {
   const fetchData = async () => {
   const result = await axios('https://hn.algolia.com/api/v1');
   setData(result.data);
};
  fetchData();
}, []);

주석: 콜백자체를 비동기로 전달하지말고 동기과정에서 비동기함수를 선언하고 호출하라는 것 같습니다.

useState훅을 여러번 사용하기

const [user, setUser] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');

우리는 React 훅이 State 객체 내부의 요소가 변경되는 것을 감지하지 못하기 때문에(주석: 객체는 mutable이기 때문에 비교했을 때 id가 동일하기 때문) 객체 하나에 여러 요소를 넣기보다 여러번 useState훅을 사용합니다. 클래스 기반 컴포넌트에서는 setState가 변경된 새로운 value를 기존 state객체에 합치고 만약 value가 변화하지 않은 key가 있다면 해당 key들의 value들은 변경하지 않습니다. 하지만 useState훅은 항상 value의 값을 교체합니다. 만약 우리가 모든 state들의 프로퍼티를가진 mutable 객체를 useState훅에 전달했다면 setState로 업데이트마다 매번 객체 전체를 교체해야합니다. 그래서 우리는 여러개의 훅에 state를 나누어야 하며 이렇게 해서 react 엔진은 독립적으로 state를 관리할 수 있습니다.

저는 당신이 react 프로그래밍 슴관을 개선하기 위해서 이 기사를 발견했으면 좋겠습니다. 만약 제가 몇가지 anti-pattern들을 놓쳤다면 코멘트로 남겨주십시오.

또한 react의 anti-pattern들을 알 수 있도록 도와준 AjayFusemachines의 JS 팀에게도 감사를 표합니다.

@jaryapp jaryapp added the docs 문서 label Nov 29, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs 문서
Projects
None yet
Development

No branches or pull requests

2 participants