List & Key

김동현·2021년 12월 9일
0

React

목록 보기
6/27
post-thumbnail

List

JSX 문법의 Content 영역에 작성된 내용들은 모두 props.children 프로퍼티에 바인딩됩니다.

만약 Content 영역에 하나의 값만 존재하는 경우 props.children에는 하나의 값이 존재하지만, 만약 여러 값이 존재하는 경우에는 props.children에는 배열로서 여러 값들을 요소로 갖고 있습니다.

그러므로 JSX 문법의 Content 영역에 배열을 작성하는 경우에는 여러 값들을 나란히 작성한 것과 동일한 동작을 합니다.

const ListComponent = () => {
    return (
        <ul>
            { [<li>kim</li>, <li>park</li>, <li>lee</li>] }
        </ul>
    );
};

위에 작성된 ListComponent 컴포넌트는 아래와 동일한 코드입니다.

const ListComponent = () => {
    return (
        <ul>
            <li>kim</li>
            <li>park</li>
            <li>lee</li>
        </ul>
    );
};

key prop

"key는 리액트가 어떤 항목을 변경, 추가 또는 삭제하는 것을 도와준다. key는 엘리먼트에 안정적인 고유성을 부여하기 위한 배열 내부의 엘리먼트(혹은 컴포넌트)에 지정한다." 라고 리액트 공식문서에서 설명하고 있습니다.

리액트는 새로운 가상돔을 이전 가상돔과 비교하여 차이나는 부분만 계산하여 실제 돔에 반영하는 방식으로 렌더링 작업을 진행합니다.

이때 이전 가상돔과 새로운 가상돔을 비교하는 과정에서 리액트 엘리먼트의 type 프로퍼티 값이 서로 다른 경우에만 리액트가 서로 다른 가상돔으로 인식하여 해당 가상돔을 새로운 가상돔으로 교체합니다.

만약 type 프로퍼티값이 같은 경우에는 이전 가상돔을 제거하지 않고 서로 다른 프로퍼티 값만을 업데이트하는 방식을 사용합니다.

이러한 방식을 사용하여 가상돔을 서로 비교할 때, 가상돔의 같은 계층에 여러 엘리먼트가 동일한 type 프로퍼티 값을 갖고 있다면 리액트는 모두 동일한 엘리먼트로 인식하여 리액트 엘리먼트의 프로퍼티 값만 변경하게 됩니다.


import React from 'react';

const MyComponent = () => {
    const [arr, setArr] = useState([
        {name: 'kim'},
        {name: 'lee'},
        {name: 'Son'}
    ]);
    
    const [enteredName, setEnteredName] = useState('');
    
    const inputChangeHandler = event => {
        setEnteredName(event.target.value);
    };
    
    // 2. addItemHandler 호출
    const addItemHandler = () => {
        const newItem = {name: enteredName};
        
        // 3. setArr 상태 변경 함수 호출
        setArr(prevState => [newItem, ...prevState]);
    };
    
    return (
        <>
            <ul>
                {arr.map(item => {
                    return <li>{item.name}</li>;
                })}
            </ul>
            <form onSubmit={addItemHandler}>
                <input type="text" value={enteredName} onChange={inputchangeHandler} />
                
                // 1. submit 버튼 클릭
                <button type="submit">Add</button>
            </form>
        </>
        );
}

텍스트 인풋 필드에 'Park'을 입력하고 submit 버튼을 누르면 addItemHandler 이벤트 핸들러가 호출됩니다.

addItemHandler 함수 내부에서는 newItem 객체를 생성하고 setArr 상태값 변경 함수를 호출하는데 인수로 newItem을 첫 번째 요소로 추가한 새로운 배열을 전달합니다.

상태값이 변경되어 리액트가 컴포넌트 함수를 재실행하는데 이때 생성된 가상돔과 이전 가상돔을 비교하여 실제 돔을 업데이트합니다.


아래는 기존 상태값 arr로 생성한 가상돔과 갱신된 상태값 arr로 생성한 가상돔을 서로 비교하는 과정입니다.

참고로 아래 작성된 가상돔은 ul을 루트로 갖는 가상돔이며, ul 엘리먼트의 props.children 값입니다.

// 이전 가상돔의 props.children 값
// 이전 arr => [{name: 'Kim'}, {name: 'Lee'}, {name: 'Son'}]
[
    {
        type: 'li',
        props: {
            children: 'Kim'
        }
    },
    {
        type: 'li',
        props: {
           chlidren: 'Lee'
        }
    },
    {
        type: 'li',
        props: {
            children: 'Son'
        }
    }
]

// 새로운 가상돔의 props.children 값
// 새로운 arr => [{name: 'Park'}, {name: 'Kim'}, {name: 'Lee'}, {name: 'Son'}]
[
    {
        type: 'li',  // -> type 값 변경되지 않음
        props: {
            children: 'Park'  // 'Kim' -> 'Park'으로 변경
        }
    },
    {
        type: 'li',  // -> type 값 변경되지 않음
        props: {
           chlidren: 'Kim'  // -> 'Lee' -> 'Kim'으로 변경
        }
    },
    {
        type: 'li',  // -> type 값 변경되지 않음
        props: {
            children: 'Lee'  // -> 'Son' -> 'Lee'으로 변경
        }
    },
    {               // -> 리액트 엘리먼트 새롭게 생성!
        type: 'li',
        props: {
            children: 'Son'
        }
    }
]
  1. 인덱스가 0인 첫 번째 항목의 type 프로퍼티값이 서로 같고, props.children 프로퍼티 값이 서로 다르므로 props.children 값만 Park으로 변경.

  2. 인덱스가 1인 두 번째 항목의 type 프로퍼티값이 서로 같고, props.children 프로퍼티 값이 서로 다르므로 props.children 값만 Kim으로 변경.

  3. 인덱스가 2인 세 번째 항목의 type 프로퍼티값이 서로 같고, props.children 프로퍼티 값이 서로 다르므로 props.children 값만 Lee으로 변경.

  4. 인덱스가 4인 항목이 없으므로 새로운 리액트 엘리먼트를 생성.

여기서 알 수 있는 점으로 우리는 새로운 항목을 배열 선두에 하나만 추가했지만, 각 엘리먼트의 type 프로퍼티 값이 같기 때문에 각 리액트 엘리먼트를 구별할 수 없으며 리액트는 그외 다른 항목의 "내용만을 변경"합니다.

현재 결과는 정확하게 나오지만 성능적인 측면에서 좋지 않습니다. 우리의 예상대로라면 선두에 새로운 항목이 추가된 배열을 리액트에게 알려주면 리액트는 배열의 선두에 새로운 항목을 추가하여 배열을 업데이트할 것이라고 생각하지만 실제로는 모든 배열의 항목을 확인하여 비교하여 "변경된 항목의 내용을 업데이트"하고, 배열의 마지막에 새로운 요소를 생성합니다.

이는 버그로도 이어질 수 있습니다. 만약 각 컴포넌트들이 각자 상태를 갖고있는 경우, 우리가 배열 선두에 추가된 컴포넌트는 이전 선두의 컴포넌트가 갖고 있던 상태를 갖게됩니다. 즉, 이전 선두에 존재하던 컴포넌트는 상태값을 잃어버리게 됩니다.


우리는 이를 해결하기 위해 각 리액트 엘리먼트에게 "고유한 값을 부여"하여 각 엘리먼트들을 리액트가 식별할 수 있도록 만들어주어야 합니다.

이때 특별한 어트리뷰트인 key를 어트리뷰트로 작성해줍니다. 이때 key 에 고유한 값을 전달하면 리액트는 각 엘리먼트를 같은 엘리먼트가 아닌 다른 엘리먼트로 "식별"할 수 있게 됩니다.

key를 이용해 고유한 값을 전달하면 리액트는 배열의 모든 엘리먼트들을 "고유하게 식별"할 수 있기 때문에 배열에 엘리먼트가 추가나 삭제되더라도 어디에 엘리먼트가 추가 삭제되었는지 리액트가 판단할 수 있습니다.

이는 리스트를 더 효율적으로 업데이트하도록 도와줍니다.

// 이전 가상돔
// 이전 arr => [{name: 'Kim'}, {name: 'Lee'}, {name: 'Son'}]
[
    {
        type: 'li',
        key: 'm1',  // -> 고유한 식별자 작성
        props: {
            children: 'Kim'
        }
    },
    {
        type: 'li',
        key: 'm2',  // -> 고유한 식별자 작성
        props: {
           chlidren: 'Lee'
        }
    },
    {
        type: 'li',
        key: 'm3',  // -> 고유한 식별자 작성
        props: {
            children: 'Son'
        }
    }
]

// 새로운 가상동
// 새로운arr => [{name: 'Park'}, {name: 'Kim'}, {name: 'Lee'}, {name: 'Son'}]
[
    {
        type: 'li',
        key: 'm4',  // -> 이전 가상돔에는 존재하지 않았던 key prop 값으로 리액트가 새로운 리액트 엘리먼트로 인식하여 새롭게 생성하여 추가
        props: {
            children: 'Park' 
        }
    },
    {
        type: 'li',
        key: 'm1',
        props: {
           chlidren: 'Kim'
        }
    },
    {
        type: 'li',
        key: 'm2',
        props: {
            children: 'Lee'
        }
    },
    {
        type: 'li',
        key: 'm3',
        props: {
            children: 'Son'
        }
    }
]

위 예제에서 key를 각 엘리먼트에 작성하여 각 엘리먼트를 리액트가 식별할 수 있습니다. 그러므로 우리가 배열 선두에 요소를 추가한다면 리액트는 key 를 통해 이전에는 없었던 리액트 엘리먼트로 인식하여 선두에 새로운 리액트 엘리먼트를 생성합니다.

재조정

리액트 엘리먼트로 가상돔을 만들고 이전 가상돔과 비교하여 차이가 존재하는 부분만 계산하여 실제 돔에 반영합니다. 가상돔은 트리 형태이고 두 개의 트리를 비교하는 로직은 O(n3)만큼 계산 복잡도를 갖게 됩니다. 이는 매우 비효율적이며 화면 렌더링을 오히려 더 느리게 만들 수도 있습니다.

그래서 두 가상돔을 비교할 때 "Diffing Algorithm"을 사용하여 차이나는 부분을 파악합니다.

1. 리액트 엘러먼트의 type 프로퍼티가 서로 다른 경우

서로 다른 가상돔으로 판단하여 교체. 이때 기존 가상돔을 완전히 제한 뒤 새로운 가상돔으로 교체하게 되므로 자식 노드까지 모두 바뀌게 됩니다.

즉, 어떤 가상돔의 루트 노드 type 프로퍼티가 다르면 해당 루트 노드의 가상돔 전체를 새로운 가상돔으로 교체하게 됩니다. 그러므로 자식 노드의 경우 따로 비교하지 않습니다.

// Previous VDOM
<p>
    <h2>,,,</h2>
    <span>,,,</span>
</p>



// Current VDOM
<section>  // -> div를 루트 엘리먼트로 하는 가상돔으로 교체
    <h2>,,,</h2>
    <span>,,,</span>
    <strong>,,,</strong>
</section>

위의 경우 어떤 가상돔의 루트 노드의 type 프로퍼티 값이 p에서 section으로 변경되었기 때문에 새로운 가상돔으로 교체하게 됩니다.

즉, 기존 p 엘리먼트를 루트 노드로 갖는 가상돔 전체를 버리고 새로운 section 엘리먼트를 루트 노드로 갖는 가상돔으로 교체하게 됩니다.

이때 하위에 노드들도 모두 새롭게 교체되므로 서로 비교하지 않습니다.

2. 리액트 엘리먼트의 type 프로퍼티 값이 같은 경우

리액트 엘리먼트의 이외 데이터(어트리뷰트)들은 값만 업데이트되며 가상돔 자체가 교체되지 않음.

예를 들어, className 값이 변경되었다고 엘리먼트 전체를 교체하지 않고 className 값만 교체하게 됩니다.

// Previous VDOM
<section className="section">
    <Counter />
</section>



// Current VDOM
<section className="counter-section">  // -> props.className값을 section -> counter-section으로 변경
    <Counter />
</section>

가상돔의 루트 노드인 type이 서로 같기 때문에 교체는 하지 않으며, 단지 className 어트리뷰트 값만 "section"에서 "counter-section"으로 업데이트만 합니다.

3. 동일한 계층에 여러 자식 엘리먼트가 존재하는 경우

루트 노드의 type 프로퍼티값이 동일한 경우 하위 노드를 위에서 사용한 방법을 사용하여 서로 비교합니다. 하위 노드들은 모두 루트 노드의 props.children에 존재하며 하나의 값을 가질 수도 있고, 여러 값을 갖는 경우에는 배열을 갖고 있습니다.

만약 여러 엘리먼트를 갖는 경우, 즉 props.children의 값으로 배열을 갖는 경우에는 각 리스트를 순차적으로 비교하는데 각 엘리먼트에 고유한 값을 갖는 key 어트리뷰트를 작성한다면 type 프로퍼티 값이 같더라도 리액트가 서로 다른 엘리먼트로 인식하여 효율적으로 렌더링이 가능합니다. 이는 type이 아닌 key 값으로 각 엘리먼트들을 식별하게 됩니다.

즉, type 프로퍼티가 동일한 여러 엘리먼트를 서로 비교할 때 각 엘리먼트에게 key 어트리뷰트값을 통해 각 엘리먼트를 고유하게 식별할 수 있으며, key 값으로 엘리먼트가 추가되거나 삭제되는 경우 어느 위치에 추가 삭제될 지 리액트가 판단할 수 있으며, 동일한 key 값을 갖는 엘리먼트의 위치가 서로 다른 경우에는 위치만을 이동하고, 프로퍼티 값이 변경된 경우에는 프로퍼티 값만 변경시키게 됩니다.

// Previous DOM
<ul>
    <li>first</li>
    <li>second</li>
    <li>third</li>
</ul>



// Current VDOM
<ul>
    <li>title</li>  // -> prop.children값을 first -> title로 변경
    <li>first</li>  // -> prop.children값을 first -> second로 변경
    <li>second</li>  // -> prop.children값을 second -> third로 변경
    <li>third</li>  // -> li 엘리먼트 새롭게 생성하여 추가
</ul>

위 가상돔에서는 루트 노드의 type 프로퍼티값이 ul로 동일하므로 하위 노드들을 비교하는데 첫 번째부터 순차적으로 순회하면서 서로 비교하게 됩니다.

  1. 리액트는 첫 번째 요소의 type은 같고 props.children 내용만 변경되었다고 판단하여 props.children 값만 변경합니다.

  2. 두 번째 엘리먼트를 서로 비교할 때도 props.children 값만 변경되었으므로 해당 값만 업데이트합니다.

이러한 동작을 하다가 마지막에는 li 엘리먼트를 새롭게 생성하여 마지막 엘리먼트로 추가합니다. 즉, 4번의 변경이 이루어집니다.

효율적으로 비교하기 위해서 key 어트리뷰트를 사용한다면 아래와 같이 업데이트가 진행됩니다.

// Previous VDOM
<ul>
    <li key="m1">first</li>
    <li key="m2">second</li>
    <li key="m3">third</li>
</ul>



// Current VDOM
<ul>
    <li key="mt">title</li>  // -> li 엘리먼트 새롭게 생성
    <li key="m1">first</li>  // -> 요소 위치만 이동
    <li key="m2">second</li>  // -> 요소 위치만 이동
    <li key="m3">third</li>  // -> 요소 위치만 이동
</ul>

리액트가 순서대로 순회하면서 각 엘리먼트를 서로 비교할 때 첫 번째 엘리먼트의 key 값이 다르기 때문에 리액트는 새로운 요소가 추가되었다고 인식하여 새롭게 생성하고 첫 번째 엘리먼트로 추가합니다. 이후 key 값이 같은 엘리먼트는 위치만 변경하게 됩니다.

즉, 같은 계층에 존재하는 여러 엘리먼트의 각 엘리먼트를 고유하게 식별하기 위해 type 프로퍼티값이 아닌 "key 프로퍼티값"을 사용하여 리액트에게 각 요소를 식별할 수 있도록 해주어야 합니다.

profile
Frontend Dev

0개의 댓글