데이터 컬렉션에서 여러개의 비슷한 컴포넌트를 보여주고 싶을 때가 있어요. 데이터 배열을 다룰 때 자바스크립트 배열 메서드를 사용할 수 있어요. 이 장에서는 데이터 배열을 컴포넌트 배열로 필터링하고 변환하기 위해 리액트에서 filter()
와 map()
을 사용헤 볼 거예요.
이 페이지에서는
- 자바스크립트의map()
을 사용하여 컴포넌트에 배열을 어떻게 렌더링하는지
- 자바스크립트의filter()
를 사용하여 특정 컴포넌트만을 어떻게 렌더링하는지
- 리액트 키를 언제 그리고 왜 사용하는지
를 알아볼 거예요.
Rendering data from arrays | 배열에서 데이터 렌더링하기
여기 콘텐츠 목록이 있어요.
<ul>
<li>Creola Katherine Johnson: mathematician</li>
<li>Mario José Molina-Pasquel Henríquez: chemist</li>
<li>Mohammad Abdus Salam: physicist</li>
<li>Percy Lavon Julian: chemist</li>
<li>Subrahmanyan Chandrasekhar: astrophysicist</li>
</ul>
이 리스트 아이템들의 다른 점은 콘텐츠, 즉 데이터 뿐이에요. 댓글부터 프로필 이미지의 갤러리까지, 인터페이스를 만들 때 다른 데이터를 사용하여 같은 컴포넌트 인스턴스를 보여줘야할 때가 있어요. 이러한 상황에서 자바스크립트 객체와 배열에 데이터를 저장하고 여기에서 컴포넌트들을 렌더링하기 위해 filter()
와 map()
와 같은 메서드를 사용할 수 있어요.
배열에서 아이템의 목록을 어떻게 생성하는지 간단한 예시를 통해 알아볼게요.
1. 데이터를 배열로 옮기세요.
const people = [
'Creola Katherine Johnson: mathematician',
'Mario José Molina-Pasquel Henríquez: chemist',
'Mohammad Abdus Salam: physicist',
'Percy Lavon Julian: chemist',
'Subrahmanyan Chandrasekhar: astrophysicist'
];
2. people
의 멤버들을 listItems
라는 새로운 JSX 노드 배열에 매핑하세요.
const listItems = people.map(person => <li>{person}</li>);
3. listIem
을 <ul>
로 감싸서 반환하세요.
return <ul>{listItems}</ul>;
그러면 결과적으로 이렇게 나와요.
위의 샌드박스는 콘솔에 에러를 표시해요.
콘솔
Warning: Each child in a list should have a unique “key” prop.
경고: 리스트에 있는 각각의 자식은 고유의 "키" prop을 가져야해요.
이 에러를 어떻게 고쳐야하는지는 이 페이지의 뒤쪽에서 배울 거예요. 그 전에, 데이터에 구조를 추가해볼게요.
Filtering arrays of items | 배열 아이템 필터링하기
이 데이터는 조금 더 구조화될 수 있어요.
const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
}, {
name: 'Percy Lavon Julian',
profession: 'chemist',
}, {
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
}];
여기서 전공이 'chemist'
인 사람들만을 보여주고 싶어요. 조건에 맞는 사람들만을 반환하려면 자바스크립트의 filter()
메서드를 사용할 수 있어요. 이 메서드는 아이템을 가진 배열을 받아서 (true
나 false
를 반환하는 함수인) "test"에 이들을 전달하고, 해당 테스트를 통과한(true
가 반환된) 아이템만으로 구성된 새로운 배열을 반환해요.
profession
이 'chemist'
인 아이템만을 원한다면 "test" 함수는 (person) => person.profession === 'chemist'
과 같을 거예요. 이것들을 넣는 예시를 보여줄게요.
1. person.profession === 'chemist'
로 필터링하는 filter()
함수를 people
에서 호출하여 "화학자"인 사람들만을 가진 새로운 배열, chemist
를 생성하세요.
const chemists = people.filter(person =>
person.profession === 'chemist'
);
2. chemists
을 매핑하세요.
const listItems = chemists.map(person =>
<li>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
);
3. listItems
를 반환하세요.*
return <ul>{listItems}</ul>;
함정
화살표 함수는 암묵적으로 => 직후에 표현식을 반환해요. 그렇기 때문에 return 구문이 필요하지 않아요.
const listItems = chemists.map(person => <li>...</li> // 암묵적으로 반환해요! );
그러나 만약=>
뒤에{
중괄호가 온다면return
을 명시적으로 작성해줘야해요.const listItems = chemists.map(person => { // 중괄호 return <li>...</li>; });
=> {
를 포함하는 화살표 함수는 "블룩 본문"를 가져야해요. 블록 본문은 한 줄 이상의 코드를 작성할 수 있게 해주지만return
구문을 직접 작성해야해요. 만약 작성하는 것을 잊어버렸다면 아무것도 리턴되지 않아요!
Keeping list items in order with key
| key
를 사용하여 순서를 가진 리스트 아이템 유지하기
위에 있던 모든 샌드박스가 콘솔에서 에러를 띄운다는 사실에 주목하세요.
콘솔
Warning: Each child in a list should have a unique “key” prop.
경고: 리스에 있는 각각의 자식은 고유의 "키" prop을 가져야해요.
각각의 아이템에는 key
를 부여해야해요. key
는 배열의 다른 아이템과 구별되는 고유한 문자열이나 숫자예요.
<li key={person.id}>...</li>
NOTE
JSX 엘리먼트는 map() 호출 안에서 항상 키가 필요해요.
키는 리액트에게 각각의 컴포넌트가 어떤 아이템과 연관되어 있는지를 알려주기 때문에 나중에 매칭할 수 있어요. 만약 배열 아이템이 (정렬과 같은 이유로) 위치가 이동하거나, 삽입되거나 삭제될 때 중요해져요. 잘 만들어진 key
는 리액트가 어떤 일이 정확히 일어났는지 추론하고 DOM 트리를 알맞게 업데이트하도록 도와줘요.
즉석에서 키를 생성하는 것보단 데이터에 키를 포함해야해요.
리스의 각 아이템에 여러개의 DOM 노드 보여주기
만약 각 아이템이 하나가 아니라 여러개의 DOM 노드를 렌더링해야한다면 어떻게 할건가요?
간단한 <>...</> Fragment 구문은 키를 넘기지 않기 때문에 하나의 <div> 태그로 이들을 묶거나 조금 더 길고 명시적인 <Fragment> 구문을 사용해야해요.
import { Fragment } from 'react';
// ...
const listItems = people.map(person =>
<Fragment key={person.id}>
<h1>{person.name}</h1>
<p>{person.bio}</p>
</Fragment>
);
Fragment는 DOM에서 사라지기 떄문에 <h1>
, <p>
, <h1>
, <p>
등등의 태그 목록만을 만들어요.
Where to get your key
| 키를 가져오는 곳
데이터의 각기 다른 소스는 키의 각기 다른 소스를 제공해요.
- 데이터베이스에서 가져온 데이터: 만약 데이터가 데이터베이스에서 왔다면 원래부터 고유한 데이터베이스의 키나 아이디를 사용할 수 있어요.
- 로컬에서 생성한 데이터: 만약 (필기 어플에서 만들어진 필기노트와 같이) 데이터가 로컬에서 생성되어서 유지된다면 아이템을 생성할 때
crypto.randomUUID()
나uuid
와 같은 패키지인 증가하는 카운터를 사용하세요.
Rules of keys | 키의 규칙
- 키는 형제들 사이에서 고유해야해요. 하지만 다른 배열에 있는 JSX 노드에 같은 키를 사용하는 것은 괜찮아요.
- 키는 바꿀수 없어요. 만약 키가 바뀐다면 본래의 목적을 달성할 수 없어요. 렌더링하는 동안 새롭게 생성하지 마세요.
Why does React need keys? | 리액트는 왜 키가 필요하나요?
데스크톱에 있는 파일에 이름이 없다고 생각해보세요. 대신, 첫 번째 파일, 두 번째 파일과 같이 순서대로 참조돼요. 이 참조를 이용할 수는 있찌만 만약 파일을 삭제한다면 혼란스러울 거예요. 두 번쨰 파일이 첫 번째 파일이 되고 세 번째 파일이 두 번째가 되는 순서로 쭉 이어질 거니까요.
폴더 안에 있는 파일 이름과 배열 안에 있는 JSX 키는 같은 목적을 수행해요. 이것들은 형제들 사이에서 각각의 항목이 고유하게 식별되도록 도와줘요. 잘 선택된 키는 배열 안에서의 위치보다 더 많은 정보를 제공해요. 심지어는 순서가 재조정되어 위치가 바뀌더라도 key
는 리액트가 생명주기동안 아이엠을 식별하도록 해줘요.
함정
배열에서 아이템의 인덱스를 키로 사용하고 싶을 수도 있어요. 사실, 만약 key를 아예 지정하지 않았을 때 리액트가 사용하는 방법이에요. 하지만 아이템이 추가되거나 삭제되거나 혹은 배열의 순서가 재배치된다면 아이템을 렌더링하는 동안 순서가 변해요. 키로 인덱스를 사용하는 것은 종종 미묘하고 혼돈스러운 버그를 만들어요.
비슷하게,key={Math.random()}
등을 사용하여 즉석에서 키를 생성하지 마세요. 이는 렌더링 동안 키가 매칭되지 않게 만들고 모든 컴포넌트와 DOM이 매 순간 재생성되도록 만들어요. 이렇게 되면 느릴 뿐만 아니라 리스트 아이템 안에서 받은 사용자 입력을 잃어버릴 거예요. 대신, 데이터에 기반한 안정적인 ID를 사용하세요.
컴포넌트는key
를 prop으로 받지 않는다는 점을 기억하세요. 키는 오직 리액트 자체 내에서 힌트로 사용돼요. 만약 컴포넌트에 ID가 필요하다면<Profile key={id} userId={id} />
와 같이 분리된 prop으로 전달해야해요.
Recap | 요약
이 페이지에서 여러분은
- 어떻게 컴포넌트 외부에서 데이터를 가져와서 배열과 객체와 같은 자료구조로 만드는지
- 어떻게 자바스크립트의
map()
으로 비슷한 컴포넌트 세트를 생성하는지 - 어떻게 자바스크립트의
filter()
로 필터링된 아이템 배열을 생성하는지 - 컬렉션 안에서 각각의 컴포넌트에
key
를 설정하여 위치나 데이터가 변하더라도 각각의 컴포넌트를 어떻게 그리고 왜 리액트가 추적할 수 있는지
를 배웠어요.
Challenges | 도전 과제
1. 리스트를 2개로 나누기
이 예시는 모든 사람들의 리스트를 보여줘요.
이 리스트를 하나 다음에 다른 하나, 여기서는 화학자와 그 외의 것들이 보이도록 두 개의 리스트로 분리하세요. 이전과 같이 person.profession === 'chemist'
인지를 확인하여 해당 사람이 화학자인지를 결정하세요.
filter()
을 두 번 사용하여 개별적인 배열을 두개 만들고 map
을 사용할 수 있어요.
이 답안에서 map
은 부모 엘리먼트인 <ul>
안에 인라인으로 바로 호출돼요. 하지만 만약 조금 더 읽기 쉬운 코드를 표방한다면 이들을 배열로 바꿀 수 있어요.
렌더링되는 리스트 사이에 약간의 중복 코드가 여전히 남아있어요. 더 발전시켜서 <ListSection>
컴포넌트로 반복되는 부분을 추출할 수 있어요.
굉장히 주의깊은 사람이라면 두 개의 filter
호출을 사용하여 각 사람의 전공을 두번 체크한다는 것을 알아차릴 거예요. 속성을 체크하는 것은 굉장히 빠르고 이 예시에서는 괜찮아요. 만약 로직이 조금 더 비싸다면 filter
호출을 반복문으로 대체하여 수동으로 배열을 구성하고 사람들을 한 번만 체크할수 있어요.
만약 people
이 절대 변하지 않는다면 컴포넌트바깥으로 이 코드를 움직여도 돼요. 리액트의 관점에서 중요한 것은 '결국 마지막에 JSX 노드 배열을 사용자에게서 받는가'예요. 어떻게 배열을 생성하는지는 신경쓰지 않아요.
2. 컴포넌트 안에서 리스트 중첩시키기
주어진 배열에서 레시피 리스트를 만드세요! 배열 안에 있는 각 레시피에서 <h2>
로 이름을 보여주고 <ul>
로 재료를 나열하세요.
이 문제에서는 두 개의 다른 map
호출을 중첩해야해요.
이렇게 하면 돼요.
각 recipes
는 이미 id
필드를 갖고 있기 때문에 반복문 외부에서 key
를 사용할 수 있어요. 재료에 대한 반복문에 사용할 ID는 없어요. 그러나 같은 재료는 한 레시피에서 두 번 나열되지 않는다는 가정은 제법 합리적이기 때문에 재료 이름을 key
로 사용할 수 있어요. 아니면, ID를 추가하는 자료구조로 바꾸거나 (안전하게 재료를 재정렬할 수 없다는 주의사항과 함께) key
로 인덱스를 사용할 수도 있어요.
3. 리스트 아이템 컴포넌트를 추출하기
RecipeList
컴포넌트는 map
을 중첩하여 두 번 호출하고 있어요. 이를 간단하게 만들려면, id
, name
, 그리고 ingredients
를 prop으로 받는 Recipe
컴포넌트로 추출하세요. 바깥쪽의 key
를 어디에 배치할 것이고, 왜 그렇게 배치할 건가요?
바깥쪽의 map
에서 JSX를 복사해서 새로운 Recipe
컴포넌트에 붙여넣고 JSX를 반환하세요. 그리고나서 recipe.name
을 name
으로, recipe.id
를 id
로, 이런식으로 바꾸세요. 그리고 Recipe
에 prop으로 이들을 전달하세요.
여기서 <Recipe {...recipe} key={recipe.id} />
은 "recipe
객체의 모든 속성을 Recipe
컴포넌트에 prop으로 전달해."라고 말하는 축약된 문법이에요. 각각의 prop을 명시적으로 <Recipe id={recipe.id} name={recipe.name} ingredients={recipe.ingredients} key={recipe.id} />
라고 쓸 수도 있어요.
key
는 Recipe
에서 반환되는 루트 <div>
에서 지정되는 것이 아니라 Recipe
자체에서 지정된다는 점을 기억하세요. 이 key
는 직접적으로 주변 배열의 컨텍스트 안에서 필요하기 때문이에요. 이전에는 각각의 <div>
들을 갖고 있었지만 이제는 하나의 <Recipes>
배열을 갖고 있어요. 즉, 컴포넌트를 추출할 때 key
를 복사하고 붙여넣을 JSX 외부에 남겨두는 것을 잊지 마세요.
4. 구분자와 함께 나열하기
이 예시는 타치바나 호쿠시의 유명한 하이쿠를 렌더링했어요. 각각의 줄은 <p>
태그로 감싸져있어요. 여러분이 할 일은 <hr />
구분자를 각 문단 뒤에 넣는 거예요. 결과적으로 아래와 같은 구조를 가져야해요.
<article>
<p>I write, erase, rewrite</p>
<hr />
<p>Erase again, and then</p>
<hr />
<p>A poppy blooms.</p>
</article>
하이쿠는 3줄 밖에 안되지만 여러분의 답은 여러 줄이어도 동작해야해요. <hr />
엘리먼트는 처음과 끝이 아닌 <p>
엘리먼트 사이에서만 보여야해요!
(시의 줄 수는 절대로 재정렬되지 않기 때문에 인덱스를 키로 사용할 수 있는 드문 케이스예요.)
map
을 수동 반복문으로 변경하거나 Fragment를 사용해야해요.
<hr />
과 <p>...</p>
태그를 진행하면서 결과 배열에 넣는 수동적인 반복문을 작성할 수 있어요.
기존처럼 lines의 인덱스를 key
로 사용하는 것은 이젠 불가능해요. 왜냐하면 각각의 구분자와 문단은 이제 같은 배열에 있기 때문이에요. 그러나 각 아이템에 접미사를 사용하여 key={i + '-text'}
와 같이 구분하는 키를 부여할 수도 있어요.
다른 방법으로는 <hr />
과 <p>...</p>
를 포함하는 Fragment의 집합을 렌더링할 수도 있어요. 그러나 <>...</>
라는 축약 구문은 키를 전달해주지 않기 때문에 <Fragement>
라고 명시적으로 작성해야해요.
기억하세요. (<></>
로 보통 작성되는) Fragment는 불필요한 <div>
를 추가하지 않고 JSX 노드를 하나로 묶어줘요.