[TIL] 2022/03/11

yongkini ·2022년 3월 10일
0

Today I Learned

목록 보기
120/172

Today I Learned

리액트 컴포넌트 파일(.js) 내에 함수 바깥에 영역은 nodeJS의 글로벌 컨텍스트처럼 작동한다?

: 본래 state update or props 의 변동이 발생하면 해당 컴포넌트는 재실행이 되게 된다. 그렇게되면 함수 내의 모든 로직을 다시 실행하게 된다. 사실 함수는 호출할 때 함수 내의 로직을 매번 다시 실행하도록 설계됐기 때문에 이는 당연한 것이다. 하지만, 이러한 원리라면 컴포넌트 내에서(함수 내에서) 정의한 변수는 리렌더링을 할 때마다 새로운 값이 되기 때문에 우리가 state를 쓰듯이 쓸 수 없을 것이다(그래서 나온 것이 hooks이다). 하지만 hooks 외에도 함수 바깥에 변수를 정의해놓게 되면 해당 함수가 리렌더링(재실행)되도 그 변수는 nodeJS의 글로벌 컨텍스트에 변수가 쌓이듯이 앱을 새로고침하기 전까지는 그 값을 계속해서 유지하고 있기 때문에 이를 활용하여 개발할 수 있다.

모든 리액트 컴포넌트에서 React를 import하지 않아도 된다?

: 프로젝트를 하다보면 무의식적으로

import React from "react";
import "./ExpenseItem.css";
import ExpenseDate from "./ExpenseDate";
import Card from "./Card";

function ExpenseItem({ title, amount, date }) {
  return (
    <Card className="expense-item">
      <ExpenseDate date={date} />
      <div className="expense-item__description">
        <h2>{title}</h2>
        <div className="expense-item__price">{`$${amount}`}</div>
      </div>
    </Card>
  );
}

export default ExpenseItem;

위와 같이

import React from "react";

React를 import해서 사용해온 사람들이 있을 것이다(나..). 그러나, 버전이 업그레이드되면서 이제는 이것이 필요없어졌다. 본래 React를 import하는 이유는 babel이 JSX를 파싱하는 과정에서 React.createElement를 사용하기 때문에 이에 대해 개발자가 명시를 해줘야했기 때문이었다. 예를 들어,

     <div className="expense-item__description">
        <h2>{title}</h2>
        <div className="expense-item__price">{`$${amount}`}</div>
      </div>

위의 코드는

React.createElement("div", {
  className: "expense-item__description"
}, /*#__PURE__*/React.createElement("h2", null, title), /*#__PURE__*/React.createElement("div", {
  className: "expense-item__price"
}, `$${amount}`));

이런식으로 babel에 의해 파싱된다(참조). 하지만 더이상 이에 대해 개발자가 명시해주지 않아도 자동으로 import를 해서 처리를 하는 방식으로 업데이트 됐다고 한다(하지만 React라는 것을 auto-import하더라도 어쨌든 우리는 컴포넌트 내에서 React를 가져다 쓰고 있다는 점은 원리상 기억해야한다). 이에 더하여 'React.createElement'를 쓰기 때문에 우리는 jsx를 쓸 때 반드시 <>로 감싸거나, div태그로 감싸는등 하나의 엘리먼트만 리턴해야하는 룰을 갖게된다. 이는 React.createElement 자체가

return(
	React.createElement('h1',{},[]);
    React.createElement('h2',{},[]);
)

위와 같이 여러개를 쓸 수 없기 때문이다. return에는 반드시 하나의 element만 넣을 수 있다는 룰에 의해서 우리가 항상 fragment나 div태그 혹은 Wrapper로 감싸줘서 리턴하는 것임을 알아야 한다.

React 개발을 설계하는 단계에서는?

: 개발 설계 단계에서 섣불리 state에 대해서 생각하지 말자. 먼저, 정적인 리액트 앱을 만든다 생각하고 개발을 하고, 그 다음에 state에 대해서 생각하자. 보통 정적인 부분에 대한 개발은 고차원적인 생각을 필요로하기 보다는 수많은 타이핑을 요구하는 경우가 많다. 하지만, state 부분에 대한 개발은 타이핑은 그렇게 많지 않아도 고차원적인 설계 및 생각이 필요한 경우가 많기에 둘을 분리해서 작업해주는 것이 좋다. 둘을 병행하면 머리가 아파진다,,

useCallback?!

: 예전부터 리액트 최적화 방법 중에 하나라고만 알고 있던 useCallback에 대해서 설명해보면, 결론적으로 너무도 심플하게 함수(컴포넌트)가 리렌더링되면 함수의 특성에 맞게 해당 함수 내에 또다른 함수들(정의해놓은)을 '재생성'하게 된다. 물론, 함수를 재생성하는 것이 얼마나 큰 리소스를 투자해야한다고 그러냐..라고 할 수 있지만, 만약에 초마다 리렌더링하는 타이머 컴포넌트에 특정한 함수가 필요하다고하면 얘기는 달라질 수 있고, 어쨌든 프로그램을 최대로 효율화해야하는 것이 개발자의 목표니까!. 어쨌든 방금 말한 '재생성'을 막아주는 것이 바로 이 'useCallback'이다.

  const onRemove = useCallback(
    id => {
      // user.id 가 파라미터로 일치하지 않는 원소만 추출해서 새로운 배열을 만듬
      // = user.id 가 id 인 것을 제거함
      setUsers(users.filter(user => user.id !== id));
    },
    [users]
  );

벨로퍼트님의 블로그에서 가져온 코드인데, 위를 보면, 해당 함수는 특정 컴포넌트 내에 속해 있어서 만약에 이 컴포넌트가 리렌더링되면, 이 함수도 자동으로 재생성되게 된다. 이를 방지하기 위해 함수를 useCallback으로 감싸준 것을 볼 수 있다. 그러면 마지막에 [users]는 뭘까?. 이는 사실 내가 여태까지 매우 필요로했던(?) 기능으로 함수 내에서 state를 가져와 쓸 때, 최신 props or state 정보를 계속해서 쓰기 위해서 설정해주는 부분이다. 예를 들어, 함수는 한번 선언하면 저대로 유지가 되기 때문에 만약에 users라는 state가 중간에 업데이트가 됐다면 이를 반영해줘야한다. 그것을 반영해주기 위한 것이 '[users]' 이부분이라고 생각하면 된다.
** 이 때 배열 속의 deps 설정은 내가 생각한대로 state, props를 업데이트해서 쓰려고할 때 쓰는 용도는 아니었다. 나는 함수 내에서 state를 최신 상태로 쓰는 방법을 고민했었고, 그것이 deps&useCallback 조합인줄 알았는데 그게 아니었다. useCallback 내의 deps의 역할은 props로 함수를 내려줄 때, 특정 함수를 새로운 값으로 쓰고 싶을 때, 즉, props가 변했음을 나타내고 싶을 때(특정 props or state에 변화에 맞춰) 써주는 것이다.

React.memo?!

: 위의 useCallback과 같이 쓰이는 React.memo를 알아보기 전에 먼저, 리액트에서 리렌더링이 발생하는 상황을 살펴보자.

  • state 변경이 있을 때
  • 새로운 props이 들어올 때
  • 부모 컴포넌트가 렌더링 될 때
  • shouldComponentUpdate에서 true가 반환될 때

위와 같이 4개의 상황이 되면 리액트는 리렌더링을 하게 된다. 그러면, 여기서 부모 컴포넌트가 리렌더링될 때를 생각해보자. 부모 컴포넌트가 리렌더링 되면 당연히 자식 컴포넌트도 리렌더링된다. 하지만, 만약에 자식 컴포넌트는 리렌더링될 필요가 없는 컴포넌트라면? 이 때 사용하는 것이 React.memo 이다. React.memo로 컴포넌트를 감싸주면, 이렇게 감싸준 컴포넌트는 props가 바뀌지 않는 이상(부모 컴포넌트로부터 받은) 리렌더링되지 않는다(메모이제이션 툴이라고 생각). 이 때 props로 함수 or js object를 내려준다면, 위에서 말했다시피 리렌더링을 하면 함수를 재실행하기 때문에 함수 내의 함수, 객체 등을 재생성하게 되고, 이에 따라 props로 내려준 함수, 객체의 값 혹은 형태가 이전과 똑같아도, 즉 props가 변하지 않았음에도 결국 그것을 다르다고 판단한다(주소값으로 비교하기에). 이에 따라 자식 컴포넌트에서는 props가 변했기에 React.memo를 써서 메모이징을 했어도 리렌더링을 하게 된다. 이럴 때 또 쓰는 것이 useCallback이다(사실 useCallback은 대부분 React.memo와 함께 쓰인다). useCallback으로 함수를 감싸주고 props로 내려주면, 해당 함수의 deps에 있는 값에 변화가 있지 않은 이상 함수를 메모이징 했다가 그대로 props로 내려주기 때문에 리렌더링을 일으키지 않는다. 이에 더하여 일반 객체를 이런식으로 쓰기 위해서는 useMemo를 사용해야한다.

const obj = useMemeo(() => ({'name': 'swan'}), []);

위에 처럼 정의해준 opj를 props로 내려주면, 자식 컴포넌트 쪽에서 React.memo를 썼다고 가정했을 때 props가 실제로 예를 들어, {'name':'james'}로 바뀌지 않은 이상 리렌더링을 하지 않게 된다.

+a

: https://taegon.kim/archives/9658, https://github.com/cgoldsby/LoginCritter 여기에 있는거 재미로 구현해보자

References

profile
완벽함 보다는 최선의 결과를 위해 끊임없이 노력하는 개발자

0개의 댓글