결국 저번 글에 이어 Key와 List에 대한 글로 돌아왔습니다. 리액트에서도 배열 데이터는 매우 자주 사용되고 중요하기 때문에 자세히 알아보도록 하겠습니다. 이전에 이야기한 리스트와 Key라는 리액트 공식문서를 참고하여 기본적인 개념에 대해 알아보고, 저의 코드를 통해서 이러한 map 함수를 잘 이해하면 매개 변수 또한 컴포넌트화 시킬 수 있다는 이야기에 대해 해보겠습니다.
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
기본적인 리스트 컴포넌트입니다. 변수를 지정해주고, 그 변수의 데이터 타입은 리스트가 됩니다. 그 리스트를 도는 map
함수를 통해 나오는 숫자를 li 태그에 집어넣고 이를 ul 안에서 변수를 통해 리턴합니다.
마지막 가상 돔에 렌더링하는 함수에서 보면, 프로퍼티 이름에 numbers, 그 값에 리스트가 들어가는 아래의 코드를 볼 수 있습니다.
<NumberList numbers={numbers} />
하지만 위와 같은 방법에서는 오류가 생깁니다. 어떤 오류가 생기게 되는 것일까요?
리스트에 있는 각각의 자식들은 독특한 key 속성값을 지녀야한다고 하네요. 그렇다면, 말하는대로 코드를 다시 수정해봅시다.
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 자식 리스트에 key 속성값 부여
<li key={number.toString()}>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
이렇게 key는 리스트를 만들 떄 포함해야 하는 특수한 문자열 속성입니다. 이제 key에 대해서 본격적으로 알아보겠습니다.
key는 리액트가 무엇을 변경, 추가, 그리고 삭제할지 구별하는 데에 도움을 줍니다. 각 리스트의 엘리먼트에 고유한 key값을 부여하여 안정적으로 명령을 수행할 수 있도록 합니다.
const todoItems = todo.map((todo) =>
<li key={todo.id}>{number}</li>
);
key는 unique한 값이여야 하므로 다른 항목들과 비교해서 반드시 고유하게 식별할 수 있는 값을 주어야합니다. 많은 경우 데이터의 ID를 key로 사용합니다.
그렇다면, 이제 인풋에 입력하는 값을 가져오는 함수를 정의해야합니다. 그러기 위해 먼저 인풋에 이벤트를 입력해보도록 하죠.
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs
<li key={index}>
{todo.text}
</li>
);
그러나 위와 같이 key를 map 함수에 내장되어 있는 두 번째 매개변수인 index
를 사용할 수도 있습니다. 하지만 이는 좋은 방법이 아닌 최후의 수단이라 할 수 있습니다. 왜냐하면, 예를 들어 댓글이 추가되거나, 중간의 댓글이 삭제되어 index 값이 변화하게 될 때는 어떻게 되는 것일까요?
리스트의 이전 key 값이 리액트가 예상하여 돔에 그린 같은 컴포넌트의 key 값이 같다면 문제가 없을 겁니다. 하지만 이는 위와 같은 경우 사실이 아니게 됩니다.
Robin Pokorny’s가 작성한 글인 인덱스를 key로 사용할 경우 부정적인 영향에 대한 상세 설명을 참고하시면 좋습니다.
function ListItem(props) {
// 맞습니다! 여기에는 key를 지정할 필요가 없습니다.
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 맞습니다! 배열 안에 key를 지정해야 합니다.
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
키는 주변 배열의 context에서만 의미가 있습니다.
예를 들어 ListItem
컴포넌트를 추출한 경우 ListItem
안에 있는 li 엘리먼트가 아니라 배열의 <ListItem />
엘리먼트가 key를 가져야합니다.
쉽게 말해 map 함수 내부에 있는 엘리먼트에 key를 넣어주는 것입니다. 그리고 이렇게 map의 함수가 너무 중첩된다면 컴포넌트로 분리하는 것이 좋습니다.
이제 리스트와 Key에 대해서 조금 감이 잡히시나요? 충분히 어려울 수 있는 개념이라고 생각합니다. 마지막에 저의 코드를 보여드리면서, 어그로를 끌었던 제목에 대해서도 설명드리겠습니다.
위와 같이 main 페이지에 모든 state
값과 함수를 정의해두었습니다. 그리고 자식 컴포넌트에서는 props
를 사용하여 값을 가져오도록 했습니다. 리액트의 유지보수가 쉬울 수 있도록 단방향 데이터 흐름을 구현하게 해주었습니다.
같은 main 페이지에 Comment
라는 컴포넌트를 만들어 props
에 값을 전달해주는 코드입니다.
props
를 구조분해 할당하였고, 다시 또 props
로 CommentList
라는 컴포넌트에 전해줍니다.
드디어 map 함수가 나오는 CommentList
컴포넌트에 도착했습니다. 과연 제목에서 말했던 뜻은 무엇일까요?
{list.map((comment, index) => {
return (
<li key={index}>
<CommentOne comm={comment} />
)
})}
딱 위 부분만 코드로 남겨보았습니다. 여기서 중요하게 보셔야할 부분은 key와 컴포넌트의 속성 값입니다. 댓글의 리스트가 map을 돌면서 댓글을 하나씩 반환하게 됩니다. 하지만 여기서 고유한 key 값을 부여해야하는데 최후의 수단인 index 값을 써주었네요.
이 부분은 문제가 발생하기 전까진 괜찮지만, 위에서 언급했다시피 정말 최후의 수단으로 사용해야하므로 key에 index 값을 주는 것은 피할 수 있다면 피해야합니다.
그리고 아래의 CommentOne
이라는 댓글 하나의 컴포넌트를 보면 comm
이라는 props
에 어떤 값이 들어있나요? 바로 map의 첫 번째 인자인 currentValue
가 속성 값으로 들어가있습니다. 이렇게 되면, 댓글 하나씩을 컴포넌트화시킬 수 있게 됩니다.
사실, 그냥 보면 굉장히 간단한 로직이지만, 리액트를 처음 접하는 저의 입장에서는 함수의 매개 변수를 props
의 속성값으로 줄 수 있다는 생각이 전혀 떠오르지 않았습니다. 그래서 사실 파란별님과 약 이틀 내내 함께 고민했던 것 같습니다.
그렇다면, 댓글 하나를 컴포넌트화시킨 아래로 가보도록 합시다.
이렇게 아주 쉽게 댓글 하나를 컴포넌트화를 해주었습니다. 이번에는 key값이 필요하지 않습니다. 그 이유는 아실 거라 생각하고 혹시 모르시다면 다시 위를 보고 와주세요!
댓글을 작성하면 위와 같이 각각의 댓글 하나하나가 className
도 고유하게 들어간 것을 알 수 있습니다. 다만, 이것보다 좋은 것은 당연히 ID값이 주어지는 것이겠죠?
리액트는 역시나 어렵습니다. 공부하면 할수록 사용자에게는 매우 좋은 경험을 선사해주겠지만, 아직 저에게는 그닥 좋은 경험을 주는지는 모르겠습니다.
그러나 점점 리액트에 익숙해지고 있다는 사실은 분명합니다. 한 오류에 대해 약 이틀간 고민하고나서야 문제가 해결이 되었지만, 이번 기회를 통해 또 다시 오류를 만났을 때 많이 성장할 수 있구나를 다시금 느끼게 되었습니다.
유튜브 개발바닥에서 향로님께서 이야기하신 것처럼 "자기가 망가뜨린 장난감 수만큼 성장한다"라는 말을 잠시나마 느낄 수 있는 기회가 되었습니다.
실제로, 다음 주부터는 제가 만든 위스타그램 페이지와 백엔드가 만나 로그인/회원가입 실습을 진행하게 됩니다. 거기서도 또 분명 다른 에러와 장애를 만나게 될 것입니다.
위와 같은 마인드로 저는 계속 장난감을 망가뜨려야겠습니다. 괜찮다면, 제 동기들의 장난감도 망가뜨려 주겠습니다. 그럼 저는 또 다음 글에서 뵙겠습니다.