배경 이미지
728x90
반응형

리액트에서 JSX의 key 속성은 리스트 렌더링 시 중요한 역할을 하는데요. 이번 포스팅에서는 key 속성이 무엇인지, 왜 필요한지, 그리고 이를 어떻게 사용하는지에 대해 설명해드릴게요.

1. JSX Key 속성은 무엇인가?

리액트에서 리스트를 렌더링할 때 각 항목에 반드시 key라는 고유한 속성을 넣어줘야해요.

리스트가 업데이트되거나 순서가 바뀌면, 리액트는 key를 기준으로 어떤 항목이 변했는지, 추가되었는지, 혹은 삭제되었는지를 빠르게 알아낼 수 있어요.간단히 말해, 리스트의 각 항목에 고유한 key를 부여하지 않으면 리액트가 리스트의 변화를 인식하지 못할 수 있어요. 안정적인 업데이트를 위해, 배열 내 요소들에 key를 제공하는 것이 필수적이에요.

 

2. 리액트는 가상 돔을 이용해서 바뀐 부분만 실제 돔에 적용!!

리액트는 가상 돔을 이용해서 바뀐 부분만 실제 돔에 적용해주는데요

리액트에서는 리스트를 나열할 때 바뀐 부분만 찾을 때 어떻게 할까요?

리스트 렌더링 시, 리액트는 key를 사용해 어떤 항목이 바뀌었는지 인식합니다.

아래 이미지에서 가상 돔을 사용해 변경된 부분을 찾아내고, 그 부분만 실제 돔에 적용하는 과정을 설명하고 있어요:

 

3. key는 유니크한(고유한) 값을 넣기! (index는 비추천)

key 속성에는 항목을 고유하게 식별할 수 있는 값을 넣어야 해요.

key에 배열의 인덱스를 사용하는 것은 비추천이에요. 왜냐하면, 리스트 항목이 추가되거나 삭제되면 인덱스가 변하기 때문이죠. 이로 인해 리액트는 잘못된 항목을 업데이트할 수 있어요. 따라서 배열의 index를 key로 사용할 수는 있지만, 리스트 항목의 변경이 자주 발생하는 경우에는 key로 적합하지 않으니 비추드려요.  

 

4. key 속성을 제대로 사용하지 않으면?

좋지 않은 예시) 아래 예시처럼 

function App() {
  const items = [
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Banana' },
    { id: 3, name: 'Orange' }
  ];

  const lists = items.map(item => <li>{item.name}</li>);

  return (
    <div>
      <h2>Fruits List</h2>
      <ul>
        {lists}
      </ul>
    </div>
  );
}

리스트 항목에 key를 지정하지 않으면 다음과 같은 경고(Warning) 메시지를 볼 수 있어요.

Warning: Each child in a list should have a unique "key" prop.

이는 리액트가 각 항목을 제대로 추적할 수 없다는 신호예요. key 속성이 없거나 고유하지 않으면 리액트는 항목을 구분하는 데 어려움을 겪고, 성능이 떨어질 수 있어요.

 

올바른 예시)

function App() {
  const items = [
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Banana' },
    { id: 3, name: 'Orange' }
  ];

  const lists = items.map(item => <li key={item.id}>{item.name}</li>); // id를 key로 사용하여 고유성 보장

  return (
    <div>
      <h2>Fruits List</h2>
      <ul>
        {lists}
      </ul>
    </div>
  );
}

 

728x90
반응형

'React' 카테고리의 다른 글

[React] 리액트 동작과정과 최적화  (1) 2024.09.11
[React] Hooks란?  (3) 2024.09.09
[React] 리액트에서 불변성(Immutability) 관리의 중요성과 방법  (0) 2024.08.21
[React] State란?  (0) 2024.08.20
[React] Props란?  (0) 2024.08.16
728x90
반응형

우선 우리가 한가지 짚고 넘어가야 할 것은 re-evaluatingre-rendering은 완전히 동일한 개념이 아니다 라는 사실이다.
즉, 함수 컴포넌트가 재실행(re-execute)되고 재평가(re-evaluate)된다고 해서 무조건 리렌더링(re-render)이 일어나는 것이 아니다. 재실행 되는 것은 컴포넌트이고, 리렌더링은 실제 DOM에서 일어나는 변화이다

 

☑️ 리액트의 기본 동작 방식

  1. Re-evaluate (재평가):
    • 상태나 속성이 변경되면 컴포넌트를 다시 계산해서 새로운 가상 DOM을 만든다.
  2. Diffing (비교):
    • 새로 생성된 가상 DOM과 이전 가상 DOM을 비교해서 어떤 부분이 변했는지 찾는다.
  3. Re-render (재렌더링):
    • 변경된 부분만 실제 DOM에 반영하고, 화면을 다시 그린다.

 

React는 state, props, context 등이 변화하면 함수 컴포넌트를 재실행(re-execute) 하여, 위에서부터 차례로 재평가(re-evaluate)한다. 이렇게 재평가한 결과는 ReactDOM에게 전달되고, ReactDOM은 virtual DOM을 이용해서 전후 비교를 한 이후에, 바뀐 부분만 real DOM 에 반영하는 리렌더링을 일으킨다. 즉, 실행과 평가의 주체는 React이고 그 대상은 컴포넌트이며, 리렌더링의 주체는 ReactDOM이고 그 대상은 DOM이다. 서로 연관되어 있기는 하지만, 같은 의미도 아닐 뿐더러 꼭 함께 일어난다고 말할 수 없다. 

 

(하지만 많은 사람들이 re-rendering을 re-evaluate(재평가) 과정까지 포함하는 더 포괄적인 용어로 사용한다. 그리고 real DOM을 업데이트 하여 re-rendering 하는 것을 repaint 등으로 부를 수 있다. 그래서 이는 문맥과 상황에 맞게 받아들여야 한다)

 

☑️ React.momo

리액트의 공식 문서를 보면, React.memo에 대한 설명은 다음과 같다:

(memo를 사용하면 컴포넌트의 props가 변경되지 않은 경우 re-rendering을 건너뛸 수 있다)

 

여기서 말하는 Re-rendering을 건너뛴다는 말은 사실 정확히 말하면 Re-evaluate를 건너뛴다는 뜻이야. React.memo안 바뀐 것이 확실한 부분에 대해서는 리액트가 다시 계산하거나 비교하지 않도록 해주는 역할을 한다.

 

처음에 React.momo 공부할 때 헷갈렸던 부분이 리액트는 애초에 비교 후 변화된 부분이 없으면 랜더링을 안해 최적화를 해주는데 왜 굳이 React.memo를 추가적으로 사용해야 할까? 라고 생각했다 

 

근데 알고보니 React.memo불필요한 Re-evaluate 자체를 막아주는 역할을 한다.

부모 컴포넌트가 다시 렌더링될 때, 자식 컴포넌트의 속성(props)이 변하지 않았다면, 자식 컴포넌트는 당연히 바뀐게 없기 떄문에 자식 컴포넌트를 다시 평가(Re-evaluate)할 필요가 없다.

 

따라서 React.memo는 props가 변화하였을 때만 해당 컴포넌트를 재실행 및 재평가 하도록 컴포넌트를 메모이제이션(memoization) 한다. 즉, React.memo는 컴포넌트를 메모이제이션(memoization)하여 이전 props와 새 props가 동일하면 컴포넌트를 다시 평가하지 않도록 최적화해준다.

 

[주의]

React.memo를 사용할 때 주의해야 할 한 가지는 함수를 props로 전달하는 경우다. 함수는 참조 타입(reference value)이기 때문에, 매번 새로운 함수 인스턴스가 생성되고 메모리 주소가 달라지기 때문에 React.memo가 이를 다른 값으로 인식하게 된다.따라서 props가 그대로인 것처럼 보여도 함수가 포함되어 있다면 매번 다른 함수로 인식하게 되어, React.memo가 적용되지 않을 수 있다.

이 문제를 해결하기 위해서는 useCallback을 함께 사용해 함수를 메모이제이션하는 것이 필요하다.

 

☑️ useCallback

useCallback함수를 메모이제이션하여, 같은 함수가 불필요하게 새로 생성되지 않도록 최적화해준다. 이렇게 함으로써 함수가 props로 전달될 때 매번 새로운 함수가 생성되는 것을 방지하고, 불필요한 Re-evaluate를 줄일 수 있다.

 

예시 1: React.memo만 사용한 경우

import React, { useState } from 'react';

// 자식 컴포넌트, React.memo로 감싸져 있음
const Child = React.memo(({ onClick }) => {
  console.log("Child component rendered");
  return <button onClick={onClick}>Click me!</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    console.log("Button clicked!");
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
      <Child onClick={handleClick} />  {/* Child에 함수 전달 */}
    </div>
  );
}

export default Parent;

부모 컴포넌트가 렌더링될 때마다, handleClick 함수가 새로운 함수로 인식되기 때문에, Child 컴포넌트는 다시 렌더링된다

 

예시 2: React.memo와 useCallback을 함께 사용한 경우

import React, { useState, useCallback } from 'react';

// 자식 컴포넌트, React.memo로 감싸져 있음
const Child = React.memo(({ onClick }) => {
  console.log("Child component rendered");
  return <button onClick={onClick}>Click me!</button>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // useCallback으로 handleClick 함수 메모이제이션
  const handleClick = useCallback(() => {
    console.log("Button clicked!");
  }, []);  // 빈 배열이므로 처음에 한 번만 생성됨

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase Count</button>
      <Child onClick={handleClick} />  {/* Child에 메모이제이션된 함수 전달 */}
    </div>
  );
}

export default Parent;

 

☑️ 최적화를 남발하면 안 되는 이유

최적화는 분명히 유용하지만, 모든 컴포넌트에 적용할 필요는 없다. 최적화에도 비용이 따르기 때문이다.

React.memo로 최적화 한 컴포넌트의 경우, 기존 props 와 새로운 props 의 값을 비교해야한다. 그러기 위해서 기존의 props 값을 저장할 공간 또한 필요하다. 즉, 최적화란, 컴포넌트를 재평가하는 데 필요한 성능 비용과 props 를 비교하는 성능의 비용을 서로 맞바꾸는 것이다.
자식 컴포넌트가 매우 많고, 많이 겹쳐있는 상황이라면 최적화를 통해서 비용을 아낄 수 있겠지만, 매우 작은 앱이나 매우 작은 앱이나 간단한 컴포넌트 트리에서는 React.memo의 효과가 미미할 수 있다. 따라서 꼭 필요한 컴포넌트에만 선택적으로 적용하는 것이 좋다.
 

728x90
반응형

'React' 카테고리의 다른 글

[React] JSX Key 속성 이해하기  (1) 2024.09.12
[React] Hooks란?  (3) 2024.09.09
[React] 리액트에서 불변성(Immutability) 관리의 중요성과 방법  (0) 2024.08.21
[React] State란?  (0) 2024.08.20
[React] Props란?  (0) 2024.08.16
728x90
반응형

1. Hooks란 무엇인가?

Hooks는 React 16.8 버전에서 도입된 기능으로, 함수형 컴포넌트에서도 상태와 생명 주기를 쉽게 관리할 수 있게 해준다. 가장 자주 쓰는 두 가지 Hooks를 소개해보자(이 외에도 useMemo, useContext 등이 있다)

  1. useState: 컴포넌트의 상태(state)를 관리할 수 있는 hook . 클래스 컴포넌트에서 this.state 대신 쓰는 기능이다.
const [name, setName] = useState(''); // name이라는 상태를 만들고 초기값을 ''로 설정

 

2. useEffect: 렌더링 후 side effects를 처리할 수 있도록 설계된  hook. 컴포넌트가 처음 나타날 때나 업데이트될 때, 또는 사라질 때 특정 작업을 할 수 있게 해준다. 생명 주기를 대신한다고 생각하면 된다.useEffect는 두 번째 인자로 빈 배열([])을 넣으면 컴포넌트가 처음 나타날 때 한 번만 실행된다. 만약 상태가 변경될 때도 실행하고 싶다면, 그 상태를 배열에 넣으면 된다.

useEffect(() => { // 이 코드는 컴포넌트가 처음 나타날 때 실행된다. }, []);

 

2. React Hooks가 왜 필요한가?

먼저 React는 컴포넌트라는 작은 조각들을 모아서 화면을 만든다. 이 컴포넌트는 크게 두 가지 방식으로 만들 수 있다:

  1. 클래스 컴포넌트: 예전 방식으로 더 복잡하고 길지만, 다양한 기능을 제공한다.
  2. 함수형 컴포넌트: 더 간단하고 코드가 짧지만, 예전에는 기능이 제한적이었다.

React가 처음에는 클래스 컴포넌트만으로 복잡한 기능을 구현했지만, 코드가 길어지고 반복되는 부분이 많았다. 그래서 React 팀은 더 간단한 함수형 컴포넌트에서도 강력한 기능을 쓸 수 있도록 Hooks라는 기능을 만들었다.


3. 클래스 컴포넌트 vs 함수형 컴포넌트

클래스 컴포넌트 예시

import React, { Component } from 'react';
import Axios from 'axios';

export default class Hello extends Component {
  constructor(props) {
    super(props);
    this.state = { name: '' };  // state 초기화
  }

  // 컴포넌트가 처음 마운트될 때 (화면에 나타날 때)
  componentDidMount() {
    Axios.get('/api/user/name')
      .then(response => {
        this.setState({ name: response.data.name }); // API에서 받은 데이터를 state에 저장
      });
  }

  render() {
    return (
      <div>
        My name is {this.state.name}
      </div>
    );
  }
}

함수형 컴포넌트 + Hooks 예시

import React, { useState, useEffect } from 'react';
import Axios from 'axios';

export default function Hello() {
  const [name, setName] = useState(''); // useState로 상태 관리

  // useEffect로 생명 주기와 비슷한 기능 구현
  useEffect(() => {
    Axios.get('/api/user/name')
      .then(response => {
        setName(response.data.name);  // API에서 받은 데이터를 state에 저장
      });
  }, []); // 빈 배열을 넣으면 처음 한 번만 실행

  return (
    <div>
      My name is {name}
    </div>
  );
}

비교

  • 클래스 컴포넌트는 상태를 관리하기 위해 this.state, constructor, componentDidMount, render 와 같은 메서드를 각각 사용한다. 생명 주기 함수라는 개념도 필요하다.
  • 반면, Hooks를 사용하는 함수형 컴포넌트는 useState와 useEffect만 있으면 훨씬 짧고 간단한 코드로 같은 기능을 구현할 수 있다.

+) React 생명 주기란?

React 컴포넌트는 화면에 나타날 때, 업데이트될 때, 사라질 때 특정 작업을 할 수 있다. 이를 생명 주기라고 부른다.

  • Mounting(마운팅): 컴포넌트가 화면에 처음 나타날 때 (componentDidMount).
  • Updating(업데이트): 컴포넌트가 변경되거나 다시 그려질 때 (componentDidUpdate).
  • Unmounting(언마운팅): 컴포넌트가 화면에서 사라질 때 (componentWillUnmount).

이런 생명 주기를 클래스 컴포넌트에서는 각각의 함수로 관리했지만, Hooks를 사용하면 useEffect 하나로 해결할 수 있다.


4. Custom Hook (사용자 정의 훅)이란?

Custom Hook은 반복되는 로직을 하나의 함수로 만들어서, 여러 컴포넌트에서 쉽게 재사용할 수 있게 해주는 기능이다.

Custom Hook 예시: useAuth

import { useState, useEffect } from 'react';
import Axios from 'axios';

function useAuth() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    Axios.get('/api/user')
      .then(response => {
        setUser(response.data);
      });
  }, []);

  return user;
}

// 이 Hook을 여러 컴포넌트에서 사용할 수 있다.
export default function Profile() {
  const user = useAuth();

  if (!user) return <div>Loading...</div>;

  return <div>Welcome, {user.name}!</div>;
}

Custom Hook의 장점

  • 여러 컴포넌트에서 같은 로직을 반복하지 않고 재사용할 수 있다.
  • 코드가 훨씬 깔끔해지고 유지보수가 쉬워진다.

5. 정리

  • React Hooks는 함수형 컴포넌트에서 상태와 생명 주기를 간단하게 관리할 수 있는 도구다.
  • 클래스 컴포넌트보다 코드가 짧고 이해하기 쉽다.
  • useState로 상태를 관리하고, useEffect로 생명 주기 작업을 처리할 수 있다.
  • Custom Hook을 이용하면 여러 컴포넌트에서 반복되는 코드를 재사용할 수 있다.

 

끝😊

728x90
반응형
728x90
반응형

리액트(React)에서 상태 관리는 애플리케이션의 성능과 유지보수성에 큰 영향을 미친다. 상태를 관리할 때 불변성을 유지하는 것은 매우 중요하다. 특히 객체 타입(Object, Array)을 다룰 때는 불변성을 지키는 것이 더욱 중요하다. 그렇다면 객체 타입에서 불변성을 유지해야 하며, 어떻게 유지할 수 있을까?

1. 불변성이란?

불변성(Immutable)이란 변하지 않는 상태를 유지하는 것 . 리액트에서는 상태가 변경될 때마다 해당 컴포넌트를 다시 렌더링하는데, 이때 불변성을 유지하지 않으면 상태 변경을 감지하기 어려워지고, 의도하지 않은 버그가 발생할 수 있다.

2. 원시 타입 vs. 참조 타입

자바스크립트에서 불변성을 이해하기 위해, 기본 타입(원시 타입)과 참조 타입의 차이를 먼저 알아야 한다.

  • 원시 타입: 불변성을 가지고 있다. 값이 변경되면 새로운 메모리 주소가 할당됩니다.
    • 예: Boolean, String, Number, null, undefined, Symbol
    • 고정된 크기: 원시 타입의 값은 고정된 크기를 가지고 있으며, 메모리의 Call Stack에 저장된다.
    • 값의 변경: 값을 변경하면, 실제로는 기존 값을 변경하지 않고 새로운 메모리 위치에 새로운 값이 저장된다
    • 예를 들어, 문자열을 생각해봅시다:

 a,b 값이 바뀌면 기존 값을 수정하지 않고  새로운 메모리 위치를 참조하게 된다. 값 자체는 변하지 않고 새로운 값이 할당되는 것이다. 이게 바로 원시 타입의 불변성이다.

 

  • 참조 타입: 불변성이 없다. 값을 변경하면 같은 메모리 주소에서 변경이 일어난다.
    • 예: Object, Array
    • 참조 타입의 데이터는 크기가 고정되어 있지 않기 때문에, 메모리에서 값을 직접 저장하는 것이 아니라, Heap 메모리에 저장되고, 메모리 주소를 통해 값을 참조한다

위 코드에서 배열에 각각 100과200을 추가하면, array가 가리키는 메모리 위치는 그대로 유지되지만, 그 위치에 저장된 값이 변경된다. 즉, 배열의 원본 데이터가 수정된 것이고, 이는 불변성이 지켜지지 않은 것이다.

불변성 요약

  • 원시 타입: 값이 변경될 때 새로운 메모리 위치에 저장되므로 불변성이 자연스럽게 유지됩니다.
  • 참조 타입: 값이 변경될 때 같은 메모리 위치에서 변경이 이루어지므로, 불변성을 유지하려면 새로운 객체나 배열을 만들어야 한다.

따라서 참조 타입의 경우는 불변성에 대해 신경써줘야한다!!

3. 왜 리액트에서 불변성을 지켜야 할까?

리액트는 상태를 업데이트할 때 얕은 비교(shallow comparison)를 사용한다. 얕은 비교란, 객체의 모든 속성을 하나하나 비교하지 않고, 참조 값(메모리 주소)만 비교하여 상태 변화를 감지하는 방법이다.

불변성을 지키는 이유:

  1. 예상치 못한 오류 방지: 참조 타임에서 객체나 배열의 값이 변할 때 원본 데이터가 변경되기에 이 원본 데이터를 참조하고 있는 다른 객체에서 예상치 못한 오류가 발생할 수 있어서 프로그래밍의 복잡도가 올라간다
  2. 효율적인 상태 업데이트: 리액트에서는 상태 업데이트 시 기본적으로 참조 값을 비교하는 "얕은 비교"를 사용한다. 만약 원본 데이터를 직접 변경하면 참조 값은 동일하게 유지되기 때문에, 리액트는 이 변화를 감지하지 못할 수 있 다. 그러나 불변성을 유지하면 새로운 참조 값을 생성하므로, 리액트가 상태 변화를 정확하게 감지할 수 있고, 이로 인해 불필요한 렌더링을 방지하여 성능이 향상된다.

불변성을 지키는 방법

참조 타입에서는 값을 바꿨을 때 Call Stack 주소 값은 같으나 Heap 메모리 값만 바꿔주기 불변성을 유지할 수 없으므로 새로운 배열을 반환하는 메소드를 사용하는 것이 좋다.

spread operator (...):, map, filter, slice, reduce

↔  원본 데이터를 변경하는 메소드:  splice, push

 

1 상태 업데이트 시

const array = [1, 2, 3, 4];
const sameArray = array;
sameArray.push(5);
console.log(array === sameArray); // true

const array = [1, 2, 3, 4];
const differentArray = [...array, 5];
console.log(array !== differentArray); // false

 

2 라이브러리 사용

  • Redux Toolkit (RTK): RTK는 불변성을 유지하면서 상태를 업데이트하는 기능을 제공하여, 리덕스 상태 관리를 더욱 간단하게 만들어준다. RTK는 내부적으로 Immer.js를 사용하여 상태를 불변하게 유지한다.
  • Immer.js: Immer.js는 상태를 불변하게 처리하면서도 직관적인 코드 작성을 가능하게 해주는 라이브러리. Immer.js를 사용하면 복잡한 상태 업데이트 로직도 쉽게 작성할 수 있다.
import produce from 'immer';

const nextState = produce(currentState, draftState => {
  draftState.name = 'New Name';
});

 

결론

불변성은 리액트에서 효율적인 상태 관리와 리렌더링을 위한 핵심 개념이다. 자바스크립트의 원시 타입과 참조 타입에 대한 이해를 바탕으로 불변성을 유지하는 방법을 익히면, 리액트 애플리케이션의 안정성과 성능을 크게 향상시킬 수 있다. 데이터 변경을 추적하고, 불변성을 지키는 다양한 방법들을 활용하여, 예기치 못한 오류를 방지하고 코드의 예측 가능성을 높일 수 있다.

 

 

-끝-

728x90
반응형

'React' 카테고리의 다른 글

[React] 리액트 동작과정과 최적화  (1) 2024.09.11
[React] Hooks란?  (3) 2024.09.09
[React] State란?  (0) 2024.08.20
[React] Props란?  (0) 2024.08.16
[React] 리액트 폴더 구조 이해  (0) 2024.08.10

+ Recent posts