돌멩이 하나/에러는 미래의 연봉

[React] map() 메소드로 여러 개의 html 엘리먼트 표시할 때 JSX key 속성과 싸운 이야기

미래에서 온 개발자 2022. 11. 30. 15:24

지난주 금요일 리액트에 입문하고 리액트를 학습한지 D+6일차가 되었다. 

지금까지 React intro / SPA / state & props 총 3개의 과제를 하면서 바닐라 JS를 쓰는 것보다 리액트를 쓸 때의 간편함을 경험할 수 있었다. 마이 아고라 스테이츠 과제를 할 때 삽질했던 기억들이 좋은 반면교사가 되어주었다. 컴포넌트로 설계하면 편했을 것을... 😇

 

지금까지 배운 React의 핵심은 : 

1. 데이터를 가져온 다음,

2. map() 메소드를 돌려서 받은 데이터를 화면에 뿌려준다. 

 

1번 데이터를 받는 과정은 아직 서버와 통신하는 걸 배우지 못해서 더미 데이터로 하고 있다.

새로운 데이터가 생성되었을 때 기존 데이터와 합치는 것이 역시 관건인데 이 부분은 과제하면서 역시나 삽질을 많이 했고, 결론은 과제 상세 안내가 제대로 안 된 부분이 있어서 어제 좀 짜증이 나는 에피소드도 있었다 😇

 

다시 본론으로 돌아와서 map() 메소드를 이용해 여러 개의 html 엘리먼트를 표시할 때는 key를 지정해 주어야 한다. 

key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는 역할을 한다. key 속성 값은 가능하면 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것이 좋다. 대부분의 경우 데이터에서 제공하는 id를 key에 할당한다. 

key 속성을 넣어주지 않으면 불필요한 렌더링이 발생해서 웹 성능이 떨어지기 때문에 key를 넣지 않은 경우 React는 에러로 처리하지는 않지만 아래 이미지와 같이 콘솔 창에 warning 표시를 해준다. 

 

 

 

저 warning을 콘솔 창에서 지우고 싶어 여러 가지 시도를 하면서 배운 것들을 공유하고자 하는 것이 이번 포스팅의 목적이다. 

 

key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 한다. 

 

아래는 공식 문서에 있는 예시로, 배열 numbers에 있는 요소를 리스트로 화면에 뿌려준다. 아래 예시처럼 배열의 인덱스로 key를 지정하는 것은 최대한 지양해야 하고 '최후의 수단(as a last resort)'으로만 쓰라고 공식 문서에서도 안내하고 있다. 인덱스를 key로 사용하는 경우, 배열이 재배열될 때 컴포넌트의 state와 관련한 문제가 발생할 수 있다고 한다. 컴포넌트 인스턴스는 key를 기반으로 갱신되고 재사용되는데, 인덱스를 key로 사용하면 항목의 순서가 바뀌었을 때 key 또한 바뀌기 때문이다. 그 결과, 컴포넌트의 state가 엉망이 되거나 의도하지 않은 방식으로 바뀔 수도 있다. 

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

const root = ReactDOM.createRoot(document.getElementById('root')); 
root.render(<ul>{listItems}</ul>);

 

이걸 과제에 적용해보면 먼저 과제의 컴포넌트 구조를 대략적으로 설명해야 한다. 

/
├── /React Twittler State Props
│   ├── README.md
│   ├── /public                        # create-react-app이 만들어낸 파일
│   └── /src                           # React 컴포넌트가 들어가는 폴더
│        ├── static                    # dummyData가 들어가는 폴더
│        │    └── dummyData.js
│        ├── Pages                     # 페이지를 표시하는 컴포넌트가 들어가는 폴더
│        │    ├── About.css
│        │    ├── About.js
│        │    ├── Mypage.css
│        │    ├── Mypage.js
│        │    ├── Tweets.css
│        │    └── Tweets.js
│        ├── Components                # 단일 컴포넌트가 들어가는 폴더
│        │    ├── Tweet.css
│        │    └── Tweet.js
│        ├── App.css
│        ├── App.js
│        ├── Footer.js
│        ├── index.js
│        └── Sidebar.js
├  package.json
└ .gitignore

 

전체 트윗 목록을 보여주는 Tweets 페이지와 특정 사용자의 트윗만 볼 수 있는 MyPage 페이지에서 아래 코드와 같이 Tweet 컴포넌트를 불러오는 구조다. 

// Tweets.js 파일 중 
      <ul className="tweets">
        {tweets.map((tweet) => {
          return <Tweet tweet={tweet} />;
        })}
      </ul>
      
// MyPage.js 파일 중
      <ul className="tweets__mypage">
        {filteredTweets.map((tweet) => {
          return <Tweet tweet={tweet} />;
        })}
      </ul>

 

핵심 컴포넌트인 <Tweet />을 살펴보면 

const Tweet = ({ tweet }) => {
  const parsedDate = new Date(tweet.createdAt).toLocaleDateString("ko-kr");

  return (
  // <li> 태그의 속성으로 key를 지정해 주었다. 
    <li className="tweet" id={tweet.id} key={tweet.id}>
      <div className="tweet__profile">
        <img src={tweet.picture} />
      </div>
      <div className="tweet__content">
        <div className="tweet__userInfo">
          <div className="tweet__userInfo--wrapper">
            <span className="tweet__username">{tweet.username}</span>
            <span className="tweet__createdAt">{parsedDate}</span>
          </div>
        </div>
        <p className="tweet__message">{tweet.content}</p>
      </div>
    </li>
  );
};

5번 라인에 주석을 단 것처럼 6번 라인의 <li> 태그에 key를 지정해 주었는데도 여전히 콘솔 창의 warning이 사라지지 않았다. 

하지만 이제 알고 있지. 모든 답은 공식 문서에 있다! 

 

공식 문서의 설명을 위의 예제에 맞게 바꾸어 보면 다음과 같다.

 

Tweet 컴포넌트를 추출한 경우 Tweet 안에 있는 <li> 엘리먼트가 아니라 배열의 <Tweet /> 엘리먼트가 key를 가져야 합니다.

 

컴포넌트를 추출한 경우는 기존의 경우처럼 <li> 태그의 속성이 아닌, 컴포넌트에 key 속성을 지정해 줘야 하는 것이었다. 그래서 Tweet 내부의 <li> 엘리먼트가 아닌 Tweets.js 와 MyPage.js 파일을 아래와 같이 변경해 주었더니 드디어 warning 경고가 사라졌다. 

// Tweets.js 파일 중 
      <ul className="tweets">
        {tweets.map((tweet) => {
          return <Tweet key={tweet.id} tweet={tweet} />;
        })}
      </ul>
      
// MyPage.js 파일 중
      <ul className="tweets__mypage">
        {filteredTweets.map((tweet) => {
          return <Tweet key={tweet.id} tweet={tweet} />;
        })}
      </ul>

 

일단 콘솔 창의 경고는 사라졌지만 <Tweet />에서 key를 지정해 준다는 게 잘 이해가 되지 않았다. 저기는 자식 컴포넌트로 데이터를 내려보내는 props를 적어주는 곳 아니던가? Tweet.js 파일에서 Tweet 컴포넌트 함수의 인자로 props를 받아서 props.key를 console에 찍어보았다. 

보시는 것처럼 key 속성이 들어있긴 한데 undefined다. 컴포넌트로 추출하지 않고 <li> 요소를 직접 반환하는 공식 문서의 listItems 예제를 브라우저에서 실행한 다음 개발자 도구로 <li> 요소를 봐도 그 안에는 key 속성이 보이지 않는다. 

하지만 위에 언급한 것처럼 모든 답은 공식 문서에 있다...! 

 

React에서 key는 힌트를 제공하지만 컴포넌트로 전달하지는 않습니다. 컴포넌트에서 key와 동일한 값이 필요하면 다른 이름의 prop으로 명시적으로 전달합니다. 아래의 예시에서 Post 컴포넌트는 props.id를 읽을 수 있지만 props.key는 읽을 수 없습니다
const content = posts.map((post) =>
  <Post
    key={post.id}
    id={post.id}
    title={post.title} />
);

 

이렇게 key와 싸운 이틀 간의 이야기가 끝이 났다. 

 

💡 오늘의 결론 : 
이해가 안 되는 문제를 만날 때는 공식 문서를 벗 삼자.

 

 

📚 참고자료:

 

리스트와 Key – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

재조정 (Reconciliation) – React

A JavaScript library for building user interfaces

ko.reactjs.org