React나 Next.js로 웹 서비스를 개발하다 보면, 데이터 컬렉션(배열이나 객체)을 바탕으로 여러 개의 비슷한 컴포넌트를 화면에 그려내야 할 때가 정말 많습니다. 예를 들어 게시판의 글 목록이나, 쇼핑몰의 상품 리스트 같은 것들이죠. 이때 JavaScript 배열 메서드를 활용해서 데이터를 자유자재로 다룰 줄 알아야 해요.
이 페이지에서는 실무에서 숨 쉬듯이 사용하게 될 filter()와 map() 메서드를 React와 함께 사용하여, 여러분의 데이터 배열을 멋진 컴포넌트 배열로 필터링하고 변환하는 방법을 배울 겁니다. 자, 힘내서 시작해 볼까요?
이 문서에서 배울 내용:
map()을 사용해서 배열 안의 데이터를 컴포넌트로 렌더링하는 방법filter()를 사용해서 조건에 맞는 특정 컴포넌트들만 렌더링하는 방법key가 무엇인지, 언제 그리고 왜 꼭 써야 하는지 (정말 중요합니다!)여기 콘텐츠 리스트가 하나 있다고 가정해 볼게요.
<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>
이 리스트 항목들의 유일한 차이점은 오직 그 안에 들어있는 '콘텐츠', 즉 '데이터'뿐이에요. 여러분이 사용자 인터페이스(UI)를 만들 때, 댓글 목록부터 프로필 이미지 갤러리까지 서로 다른 데이터를 가진 동일한 컴포넌트의 여러 인스턴스를 보여줘야 하는 상황이 굉장히 자주 생깁니다.
이런 상황에서는 그 데이터를 JavaScript의 객체나 배열에 쏙 저장해 두고, map()이나 filter() 같은 훌륭한 메서드들을 사용해서 컴포넌트 리스트로 렌더링할 수 있어요.
(강사의 덧붙임: 위 이미지는 map과 filter의 작동 방식을 보여줍니다. map은 기존 배열의 모든 요소를 가공해서 똑같은 길이의 새로운 배열을 만들고, filter는 조건에 맞는 요소만 걸러내서 새로운 배열을 만들어요. 이 두 가지 개념은 데이터 가공의 핵심입니다!)
배열에서 항목 리스트를 생성하는 간단한 예를 단계별로 살펴볼게요.
const people = [
'Creola Katherine Johnson: mathematician',
'Mario José Molina-Pasquel Henríquez: chemist',
'Mohammad Abdus Salam: physicist',
'Percy Lavon Julian: chemist',
'Subrahmanyan Chandrasekhar: astrophysicist'
];
people 배열의 멤버들을 새로운 JSX 노드 배열인 listItems로 매핑합니다 (Map):const listItems = people.map(person => <li>{person}</li>);
listItems를 <ul> 태그로 감싸서 반환합니다 (Return):return <ul>{listItems}</ul>;
결과는 이렇게 나옵니다!
// App.js
const people = [
'Creola Katherine Johnson: mathematician',
'Mario José Molina-Pasquel Henríquez: chemist',
'Mohammad Abdus Salam: physicist',
'Percy Lavon Julian: chemist',
'Subrahmanyan Chandrasekhar: astrophysicist'
];
export default function List() {
const listItems = people.map(person =>
<li>{person}</li>
);
return <ul>{listItems}</ul>;
}
// styles.css
li { margin-bottom: 10px; }
그런데, 위 샌드박스를 실행해 보면 콘솔에 다음과 같은 에러(경고)가 표시되는 걸 눈치채셨을 거예요.
Warning: Each child in a list should have a unique "key" prop.
(경고: 리스트의 각 자식 요소는 반드시 고유한 "key" prop을 가져야 합니다.)
이 에러를 어떻게 고치는지는 이 페이지 뒷부분에서 자세히 알려드릴게요. 그전에, 우리가 다루는 데이터에 좀 더 구조를 더해 볼까요?
데이터를 훨씬 더 체계적으로 구조화할 수 있습니다. 객체 배열을 사용해서 말이죠.
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',
}, {
id: 3,
name: 'Percy Lavon Julian',
profession: 'chemist',
}, {
id: 4,
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
}];
자, 여기서 직업(profession)이 화학자('chemist')인 사람만 화면에 보여주고 싶다고 해볼게요. 이럴 때 바로 JavaScript의 filter() 메서드를 사용하면 그 사람들만 딱 걸러내서 반환받을 수 있어요.
이 메서드는 항목들의 배열을 가져와서 일종의 "테스트"(결과가 true나 false로 나오는 함수)를 거치게 한 다음, 그 테스트를 통과한(true를 반환한) 항목들로만 구성된 완전히 새로운 배열을 만들어 냅니다. 원본 배열은 건드리지 않고요!
우리는 profession이 'chemist'인 항목들만 원하잖아요? 이걸 위한 "테스트" 함수는 (person) => person.profession === 'chemist' 처럼 생겼을 겁니다. 이걸 어떻게 조합하는지 순서대로 볼까요?
people 배열에 person.profession === 'chemist' 조건으로 filter()를 호출해서, 화학자들만 있는 새로운 배열 chemists를 생성합니다 (Create):const chemists = people.filter(person =>
person.profession === 'chemist'
);
chemists 배열 위에서 매핑합니다 (Map):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>
);
listItems를 반환합니다 (Return):return <ul>{listItems}</ul>;
전체 코드는 아래와 같습니다.
// src/App.js
import { people } from './data.js';
import { getImageUrl } from './utils.js';
export default function List() {
const chemists = people.filter(person =>
person.profession === 'chemist'
);
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>
);
return <ul>{listItems}</ul>;
}
// src/data.js
export const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
accomplishment: 'spaceflight calculations',
imageId: 'MK3eW3A'
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
accomplishment: 'discovery of Arctic ozone hole',
imageId: 'mynHUSa'
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
accomplishment: 'electromagnetism theory',
imageId: 'bE7W1ji'
}, {
id: 3,
name: 'Percy Lavon Julian',
profession: 'chemist',
accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
imageId: 'IOjWm71'
}, {
id: 4,
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
accomplishment: 'white dwarf star mass calculations',
imageId: 'lrWQx8l'
}];
// src/utils.js
export function getImageUrl(person) {
return (
'[https://i.imgur.com/](https://i.imgur.com/)' +
person.imageId +
's.jpg'
);
}
// styles.css
ul { list-style-type: none; padding: 0px 10px; }
li {
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }
(주의하세요! 프론트엔드 초보 시절에 정말 많이 겪는 함정입니다.)
화살표 함수(Arrow function)는 => 기호 바로 다음에 오는 표현식을 암시적으로 반환(return) 합니다. 그래서 별도의 return 문을 적어줄 필요가 없었어요.
const listItems = chemists.map(person =>
<li>...</li> // 암시적 반환(Implicit return)!
);
하지만, => 뒤에 { (중괄호)가 따라온다면 반드시 return을 명시적으로 작성해주셔야 합니다! ```js
const listItems = chemists.map(person => { // 중괄호 시작!
return
`=> {` 를 포함하는 화살표 함수는 ["블록 본문(block body)"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions#function_body)을 가진다고 말합니다. 중괄호를 쓰면 코드 여러 줄을 작성할 수 있어서 좋지만, 대신 *반드시* 여러분이 직접 `return` 문을 적어줘야 한다는 규칙이 생겨요. 만약 깜빡 잊고 적지 않으면, 아무것도 반환되지 않아서 화면이 새하얗게 비어버릴 겁니다!
</Pitfall>
## `key`를 사용하여 리스트 항목의 순서 유지하기 {/*keeping-list-items-in-order-with-key*/}
아까 위의 샌드박스들을 실행했을 때 콘솔에 에러가 떴던 것 기억하시나요?
<ConsoleBlock level="error">
Warning: Each child in a list should have a unique "key" prop.
</ConsoleBlock>
*(강사의 덧붙임: React가 렌더링을 최적화하고 화면을 빠르게 업데이트하기 위해 사용하는 '가상 DOM(Virtual DOM)' 비교 알고리즘에서 `key`는 핵심적인 역할을 합니다. 데이터가 바뀌었을 때 어느 부분이 추가되고 삭제되었는지 React가 알아채는 유일한 단서거든요.)*
여러분이 `map`으로 배열 항목을 렌더링할 때, 배열의 각 항목에는 다른 항목들과 고유하게 구별될 수 있는 문자열이나 숫자인 `key`를 반드시 부여해야 합니다.
```js
<li key={person.id}>...</li>
기억해 두세요: map() 호출 내부에서 직접 렌더링되는 JSX 엘리먼트들에는 항상 key가 필요합니다!
이 key는 React에게 각각의 컴포넌트가 배열의 어느 항목에 해당하는지 알려주는 이름표 역할을 해요. 그래서 나중에 데이터를 다시 맞출 때(매칭할 때) 이 이름표를 보고 찾게 되죠.
이 개념은 배열 항목들이 이동하거나(예: 정렬 기능), 새 항목이 중간에 삽입되거나, 삭제될 수 있을 때 특히 중요해집니다. key를 잘 지정해 두면, React는 어떤 항목에 무슨 일이 일어났는지 정확히 추론하고 DOM 트리에 올바른 업데이트만 쏙쏙 적용할 수 있답니다.
key를 컴포넌트 렌더링 중에 즉석에서 만들어내기보다는, 여러분의 원본 데이터 자체에 포함시켜 두는 것이 좋습니다.
// src/App.js
import { people } from './data.js';
import { getImageUrl } from './utils.js';
export default function List() {
const listItems = people.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
);
return <ul>{listItems}</ul>;
}
// src/data.js active
export const people = [{
id: 0, // 이 값을 JSX에서 key로 사용합니다
name: 'Creola Katherine Johnson',
profession: 'mathematician',
accomplishment: 'spaceflight calculations',
imageId: 'MK3eW3A'
}, {
id: 1, // 이 값을 JSX에서 key로 사용합니다
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
accomplishment: 'discovery of Arctic ozone hole',
imageId: 'mynHUSa'
}, {
id: 2, // 이 값을 JSX에서 key로 사용합니다
name: 'Mohammad Abdus Salam',
profession: 'physicist',
accomplishment: 'electromagnetism theory',
imageId: 'bE7W1ji'
}, {
id: 3, // 이 값을 JSX에서 key로 사용합니다
name: 'Percy Lavon Julian',
profession: 'chemist',
accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
imageId: 'IOjWm71'
}, {
id: 4, // 이 값을 JSX에서 key로 사용합니다
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
accomplishment: 'white dwarf star mass calculations',
imageId: 'lrWQx8l'
}];
// src/utils.js
export function getImageUrl(person) {
return (
'[https://i.imgur.com/](https://i.imgur.com/)' +
person.imageId +
's.jpg'
);
}
// styles.css
ul { list-style-type: none; padding: 0px 10px; }
li {
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }
map을 돌리는데 각각의 항목이 단일 태그가 아니라 여러 개의 태그(DOM 노드)를 한꺼번에 렌더링해야 한다면 어떻게 해야 할까요?
짧은 형태의 <>...</> Fragment 문법은 괄호 속성에 key를 전달할 수 없게 되어 있어요. 그래서 이런 경우에는 전체를 하나의 <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> 와 같이 평평하게 이어지는 리스트를 만들어냅니다.
key는 어디서 가져와야 할까요? {/where-to-get-your-key/}여러분이 다루는 데이터의 출처에 따라 key를 가져오는 방법도 다릅니다.
crypto.randomUUID()를 호출하거나, 혹은 uuid 같은 패키지를 사용해서 고유한 키를 직접 만들어 주면 됩니다.key를 즉석에서 생성하지 마세요. (강사 메모: 데이터가 가진 고유한 식별자를 써야지 렌더링할 때마다 새로운 값을 만들어서 넣으면 안 된다는 뜻이에요.)여러분의 컴퓨터 바탕화면에 있는 파일들에 '이름'이 없다고 상상해 보세요. 대신, 첫 번째 파일, 두 번째 파일, 이런 식으로 '순서'로만 부른다고 쳐볼게요. 처음에는 익숙해질 수도 있겠지만, 중간에 파일 하나를 삭제하면 상황이 엄청 헷갈리게 됩니다. 두 번째 파일이 갑자기 첫 번째 파일이 되고, 세 번째 파일이 두 번째 파일이 되어버리니까요.
폴더 안의 파일 이름과 배열 안의 JSX key는 비슷한 역할을 합니다. 형제 항목들 사이에서 특정 항목을 고유하게 식별하게 해 주죠. 잘 선택된 key는 배열 내의 단순한 '위치(인덱스)' 정보보다 훨씬 더 많은 정보를 제공합니다. 리스트 순서가 바뀌어서 위치가 변경되더라도, React는 이 key를 보고 해당 항목이 살아있는 내내 똑같은 녀석이라는 걸 정확히 알아챌 수 있습니다.
배열의 '인덱스(index)'를 그대로 key로 쓰고 싶은 유혹에 빠지기 쉬워요. 사실 여러분이 명시적으로 key를 지정하지 않으면, React는 기본적으로 이 인덱스를 키로 사용해 버립니다.
하지만, 항목이 새로 삽입되거나 삭제되거나, 또는 배열이 다시 정렬되는 기능이 있다면 항목을 렌더링하는 순서가 시간에 따라 변하게 됩니다. 인덱스를 키로 사용하는 것은 찾기 힘들고 골치 아픈 버그를 자주 만들어 냅니다.
비슷한 이유로, key={Math.random()} 처럼 렌더링할 때마다 즉석에서 키를 랜덤하게 만들어 내는 것도 절대 피해야 합니다. 이렇게 하면 매 렌더링마다 키가 일치하지 않게 되어서, React가 컴포넌트와 DOM 전체를 매번 다 부수고 새로 만들게 됩니다. 이러면 속도도 엄청 느려지고, 리스트 항목 안에 사용자가 입력해 둔 포커스나 텍스트 같은 상태들이 다 날아가 버려요. 대신, 데이터에 기반한 안정적인 고유 ID를 사용하세요.
참고로, 여러분이 만든 컴포넌트는 이 key를 prop으로 직접 전달받지 않습니다. key는 오직 React 자체적으로 사용하는 힌트일 뿐이에요. 만약 자식 컴포넌트 내부에서 ID 데이터가 필요하다면, <Profile key={id} userId={id} /> 처럼 별도의 prop 이름으로 따로 넘겨주셔야 합니다.
이 페이지에서 배운 내용 요약:
map()을 사용해서 비슷한 형태의 여러 컴포넌트를 한꺼번에 생성하는 방법.filter()를 사용해서 필터링된 항목들의 배열을 생성하는 방법.key를 설정해야 하는지, 그리고 어떻게 설정하는지. 이를 통해 항목의 위치나 데이터가 바뀌더라도 React가 각각을 놓치지 않고 잘 추적할 수 있도록 돕는다는 사실!아래 예제는 모든 사람의 리스트를 보여줍니다.
이것을 연달아 나타나는 두 개의 분리된 리스트, 화학자(Chemists) 와 그 외의 사람들(Everyone Else) 로 나눠서 보여주도록 코드를 수정해 보세요. 아까 해본 것처럼 person.profession === 'chemist'인지 체크해서 화학자인지 아닌지 판별할 수 있습니다.
// src/App.js
import { people } from './data.js';
import { getImageUrl } from './utils.js';
export default function List() {
const listItems = people.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
);
return (
<article>
<h1>Scientists</h1>
<ul>{listItems}</ul>
</article>
);
}
// src/data.js
export const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
accomplishment: 'spaceflight calculations',
imageId: 'MK3eW3A'
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
accomplishment: 'discovery of Arctic ozone hole',
imageId: 'mynHUSa'
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
accomplishment: 'electromagnetism theory',
imageId: 'bE7W1ji'
}, {
id: 3,
name: 'Percy Lavon Julian',
profession: 'chemist',
accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
imageId: 'IOjWm71'
}, {
id: 4,
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
accomplishment: 'white dwarf star mass calculations',
imageId: 'lrWQx8l'
}];
// src/utils.js
export function getImageUrl(person) {
return (
'[https://i.imgur.com/](https://i.imgur.com/)' +
person.imageId +
's.jpg'
);
}
// styles.css
ul { list-style-type: none; padding: 0px 10px; }
li {
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }
해설:
filter()를 두 번 써서 두 개의 분리된 배열을 만들고, 그 두 배열 각각에 대해 map을 돌리면 됩니다!
// src/App.js
import { people } from './data.js';
import { getImageUrl } from './utils.js';
export default function List() {
const chemists = people.filter(person =>
person.profession === 'chemist'
);
const everyoneElse = people.filter(person =>
person.profession !== 'chemist'
);
return (
<article>
<h1>Scientists</h1>
<h2>Chemists</h2>
<ul>
{chemists.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
)}
</ul>
<h2>Everyone Else</h2>
<ul>
{everyoneElse.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
)}
</ul>
</article>
);
}
// src/data.js
export const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
accomplishment: 'spaceflight calculations',
imageId: 'MK3eW3A'
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
accomplishment: 'discovery of Arctic ozone hole',
imageId: 'mynHUSa'
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
accomplishment: 'electromagnetism theory',
imageId: 'bE7W1ji'
}, {
id: 3,
name: 'Percy Lavon Julian',
profession: 'chemist',
accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
imageId: 'IOjWm71'
}, {
id: 4,
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
accomplishment: 'white dwarf star mass calculations',
imageId: 'lrWQx8l'
}];
// src/utils.js
export function getImageUrl(person) {
return (
'[https://i.imgur.com/](https://i.imgur.com/)' +
person.imageId +
's.jpg'
);
}
// styles.css
ul { list-style-type: none; padding: 0px 10px; }
li {
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }
위 해법에서는 부모인 <ul> 엘리먼트 안에 map 호출을 인라인으로 직접 배치했어요. 코드가 좀 길다 싶으면 아까 본문 예제처럼 변수로 빼내서 쓰셔도 당연히 괜찮습니다.
그런데 코드를 보면 렌더링하는 리스트 부분에 중복된 내용이 꽤 보이죠? 한 걸음 더 나아가서, 이 반복되는 부분을 <ListSection> 이라는 공통 컴포넌트로 뽑아낼 수도 있습니다. 이렇게 컴포넌트를 잘게 쪼개는 연습을 하시는 게 좋아요!
// src/App.js
import { people } from './data.js';
import { getImageUrl } from './utils.js';
function ListSection({ title, people }) {
return (
<>
<h2>{title}</h2>
<ul>
{people.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
)}
</ul>
</>
);
}
export default function List() {
const chemists = people.filter(person =>
person.profession === 'chemist'
);
const everyoneElse = people.filter(person =>
person.profession !== 'chemist'
);
return (
<article>
<h1>Scientists</h1>
<ListSection
title="Chemists"
people={chemists}
/>
<ListSection
title="Everyone Else"
people={everyoneElse}
/>
</article>
);
}
// src/data.js
export const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
accomplishment: 'spaceflight calculations',
imageId: 'MK3eW3A'
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
accomplishment: 'discovery of Arctic ozone hole',
imageId: 'mynHUSa'
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
accomplishment: 'electromagnetism theory',
imageId: 'bE7W1ji'
}, {
id: 3,
name: 'Percy Lavon Julian',
profession: 'chemist',
accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
imageId: 'IOjWm71'
}, {
id: 4,
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
accomplishment: 'white dwarf star mass calculations',
imageId: 'lrWQx8l'
}];
// src/utils.js
export function getImageUrl(person) {
return (
'[https://i.imgur.com/](https://i.imgur.com/)' +
person.imageId +
's.jpg'
);
}
// styles.css
ul { list-style-type: none; padding: 0px 10px; }
li {
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }
눈썰미가 좋으시다면, 우리가 filter를 두 번 호출하면서 각 사람의 직업을 불필요하게 두 번씩 검사하고 있다는 걸 눈치채셨을지도 몰라요. 객체의 속성을 확인하는 작업은 매우 빠르기 때문에 이 예제 정도에서는 아무 문제 없지만, 만약 로직이 굉장히 복잡하고 무겁다면 filter 두 번 쓰는 대신 루프문 하나로 배열을 수동으로 구성해서 각 사람을 딱 한 번만 검사하도록 바꿀 수도 있습니다. (알고리즘 문제를 풀 때 성능 최적화를 하는 것과 비슷한 원리죠!)
만약 people 배열의 데이터가 프로그램 도중에 절대 변하지 않는 정적인 데이터라면, 이 데이터를 분리하는 코드를 아예 컴포넌트 바깥으로 빼버려도 됩니다. React 입장에서는 결국 마지막에 JSX 노드 배열을 받기만 하면 되거든요. 그걸 어떻게 만들어 냈는지 과정은 신경 쓰지 않습니다.
// src/App.js
import { people } from './data.js';
import { getImageUrl } from './utils.js';
let chemists = [];
let everyoneElse = [];
people.forEach(person => {
if (person.profession === 'chemist') {
chemists.push(person);
} else {
everyoneElse.push(person);
}
});
function ListSection({ title, people }) {
return (
<>
<h2>{title}</h2>
<ul>
{people.map(person =>
<li key={person.id}>
<img
src={getImageUrl(person)}
alt={person.name}
/>
<p>
<b>{person.name}:</b>
{' ' + person.profession + ' '}
known for {person.accomplishment}
</p>
</li>
)}
</ul>
</>
);
}
export default function List() {
return (
<article>
<h1>Scientists</h1>
<ListSection
title="Chemists"
people={chemists}
/>
<ListSection
title="Everyone Else"
people={everyoneElse}
/>
</article>
);
}
// src/data.js
export const people = [{
id: 0,
name: 'Creola Katherine Johnson',
profession: 'mathematician',
accomplishment: 'spaceflight calculations',
imageId: 'MK3eW3A'
}, {
id: 1,
name: 'Mario José Molina-Pasquel Henríquez',
profession: 'chemist',
accomplishment: 'discovery of Arctic ozone hole',
imageId: 'mynHUSa'
}, {
id: 2,
name: 'Mohammad Abdus Salam',
profession: 'physicist',
accomplishment: 'electromagnetism theory',
imageId: 'bE7W1ji'
}, {
id: 3,
name: 'Percy Lavon Julian',
profession: 'chemist',
accomplishment: 'pioneering cortisone drugs, steroids and birth control pills',
imageId: 'IOjWm71'
}, {
id: 4,
name: 'Subrahmanyan Chandrasekhar',
profession: 'astrophysicist',
accomplishment: 'white dwarf star mass calculations',
imageId: 'lrWQx8l'
}];
// src/utils.js
export function getImageUrl(person) {
return (
'[https://i.imgur.com/](https://i.imgur.com/)' +
person.imageId +
's.jpg'
);
}
// styles.css
ul { list-style-type: none; padding: 0px 10px; }
li {
margin-bottom: 10px;
display: grid;
grid-template-columns: auto 1fr;
gap: 20px;
align-items: center;
}
img { width: 100px; height: 100px; border-radius: 50%; }
주어진 배열을 가지고 레시피 리스트를 만들어 보세요! 배열 안의 각 레시피마다 이름은 <h2> 태그로 표시하고, 거기에 필요한 재료(ingredients)들은 <ul> 태그 안에 리스트로 보여주시면 됩니다.
이 문제를 풀려면 map 호출을 두 번 '중첩'해서 사용해야 할 거예요. 배열 안의 배열을 렌더링하는 거니까요!
// src/App.js
import { recipes } from './data.js';
export default function RecipeList() {
return (
<div>
<h1>Recipes</h1>
</div>
);
}
// src/data.js
export const recipes = [{
id: 'greek-salad',
name: 'Greek Salad',
ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
id: 'hawaiian-pizza',
name: 'Hawaiian Pizza',
ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
id: 'hummus',
name: 'Hummus',
ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];
해설:
이 문제를 해결할 수 있는 한 가지 방법은 아래와 같습니다.
// src/App.js
import { recipes } from './data.js';
export default function RecipeList() {
return (
<div>
<h1>Recipes</h1>
{recipes.map(recipe =>
<div key={recipe.id}>
<h2>{recipe.name}</h2>
<ul>
{recipe.ingredients.map(ingredient =>
<li key={ingredient}>
{ingredient}
</li>
)}
</ul>
</div>
)}
</div>
);
}
// src/data.js
export const recipes = [{
id: 'greek-salad',
name: 'Greek Salad',
ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
id: 'hawaiian-pizza',
name: 'Hawaiian Pizza',
ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
id: 'hummus',
name: 'Hummus',
ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];
recipes 배열 안의 각 항목들은 이미 id 필드를 가지고 있어서, 바깥쪽 루프(레시피 목록)에서는 이걸 key로 사용하면 완벽합니다. 그런데 안쪽의 재료(ingredients) 배열을 돌 때는 고유한 ID로 쓸 만한 값이 딱히 보이지 않죠?
하지만, 상식적으로 생각해 보면 하나의 요리 레시피 안에서 똑같은 재료 이름이 두 번 연속으로 들어갈 일은 없을 거라고 가정해도 괜찮습니다. 그래서 재료 이름 문자열 그 자체를 key로 사용해도 괜찮은 거죠. 만약 재료 이름이 겹칠 수 있는 상황이라면 데이터 구조 자체를 바꿔서 고유 ID를 추가하거나, 차선책으로 배열의 인덱스를 key로 사용할 수도 있습니다. (단, 배열 순서를 바꾸는 기능이 없다는 전제 하에요!)
방금 만든 RecipeList 컴포넌트를 보면 안에 map 호출이 두 개나 중첩되어 있어서 코드가 살짝 복잡해 보입니다. 이걸 깔끔하게 정리하기 위해, id, name, ingredients를 prop으로 받는 Recipe라는 새로운 컴포넌트를 바깥으로 추출(분리)해 보세요.
질문: 추출하고 났을 때 바깥쪽 key는 어디에 두어야 하며 그 이유는 무엇일까요?
// src/App.js
import { recipes } from './data.js';
export default function RecipeList() {
return (
<div>
<h1>Recipes</h1>
{recipes.map(recipe =>
<div key={recipe.id}>
<h2>{recipe.name}</h2>
<ul>
{recipe.ingredients.map(ingredient =>
<li key={ingredient}>
{ingredient}
</li>
)}
</ul>
</div>
)}
</div>
);
}
// src/data.js
export const recipes = [{
id: 'greek-salad',
name: 'Greek Salad',
ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
id: 'hawaiian-pizza',
name: 'Hawaiian Pizza',
ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
id: 'hummus',
name: 'Hummus',
ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];
해설:
바깥쪽 map 함수 안에 있던 JSX 덩어리를 그대로 복사해서 새롭게 만든 Recipe 컴포넌트의 return 부분에 붙여넣기 하시면 됩니다. 그리고 recipe.name은 name으로, recipe.id는 id로 고친 다음, 이 값들을 Recipe 컴포넌트에 prop으로 전달하면 되겠죠.
// src/App.js
import { recipes } from './data.js';
function Recipe({ id, name, ingredients }) {
return (
<div>
<h2>{name}</h2>
<ul>
{ingredients.map(ingredient =>
<li key={ingredient}>
{ingredient}
</li>
)}
</ul>
</div>
);
}
export default function RecipeList() {
return (
<div>
<h1>Recipes</h1>
{recipes.map(recipe =>
<Recipe {...recipe} key={recipe.id} />
)}
</div>
);
}
// src/data.js
export const recipes = [{
id: 'greek-salad',
name: 'Greek Salad',
ingredients: ['tomatoes', 'cucumber', 'onion', 'olives', 'feta']
}, {
id: 'hawaiian-pizza',
name: 'Hawaiian Pizza',
ingredients: ['pizza crust', 'pizza sauce', 'mozzarella', 'ham', 'pineapple']
}, {
id: 'hummus',
name: 'Hummus',
ingredients: ['chickpeas', 'olive oil', 'garlic cloves', 'lemon', 'tahini']
}];
여기서 쓰인 <Recipe {...recipe} key={recipe.id} /> 라는 문법은 "객체 recipe가 가지고 있는 모든 속성들을 전부 Recipe 컴포넌트의 prop으로 한 번에 넘겨줘라"라는 아주 편리한 문법적 지름길(스프레드 문법)이에요. 만약 일일이 다 쓰고 싶다면 <Recipe id={recipe.id} name={recipe.name} ingredients={recipe.ingredients} key={recipe.id} /> 처럼 하나하나 명시적으로 적어주셔도 똑같이 동작합니다.
가장 중요한 포인트는 key 속성이 Recipe 컴포넌트 내부의 루트 태그인 <div>가 아니라, 바깥쪽에서 호출되는 <Recipe> 컴포넌트 자체에 지정되었다는 점입니다! 왜 그럴까요? key는 배열을 순회하는 '그루핑된 문맥 안에서' 직접적으로 필요하기 때문입니다. 이전에는 <div>들의 배열이었으니까 각각의 <div>에 key가 필요했지만, 컴포넌트를 분리하고 난 지금은 <Recipe> 컴포넌트들의 배열이 되었잖아요? 컴포넌트를 바깥으로 분리(추출)하실 때, 복사해 가는 안쪽 코드에 key를 놔두지 말고 배열을 생성하는 바깥쪽 컴포넌트 호출부에 key를 달아주는 것 잊지 마세요.
이번 예제는 타치바나 호쿠시의 유명한 하이쿠(일본 전통시)를 렌더링하는데, 시의 각 줄이 <p> 태그로 감싸져 있습니다. 여러분이 하실 일은 각 문단 사이에 구분선 역할을 하는 <hr /> 태그를 삽입하는 겁니다. 결과적으로 생성되는 HTML 구조는 이런 모양이어야 해요.
<article>
<p>I write, erase, rewrite</p>
<hr />
<p>Erase again, and then</p>
<hr />
<p>A poppy blooms.</p>
</article>
하이쿠는 보통 3줄로 끝나는 짧은 시지만, 여러분이 작성한 솔루션 코드는 줄이 몇 개로 늘어나든 상관없이 완벽하게 동작해야 합니다. 주의할 점은, <hr /> 엘리먼트는 <p> 태그들의 맨 앞이나 맨 뒤가 아니라 오직 문단과 문단 사이 에만 나타나야 한다는 거예요!
// src/App.js
const poem = {
lines: [
'I write, erase, rewrite',
'Erase again, and then',
'A poppy blooms.'
]
};
export default function Poem() {
return (
<article>
{poem.lines.map((line, index) =>
<p key={index}>
{line}
</p>
)}
</article>
);
}
// styles.css
body {
text-align: center;
}
p {
font-family: Georgia, serif;
font-size: 20px;
font-style: italic;
}
hr {
margin: 0 120px 0 120px;
border: 1px dashed #45c3d8;
}
(참고로 이 문제는 시의 줄 순서가 뒤죽박죽 섞일 일이 전혀 없기 때문에, 배열의 인덱스를 key로 사용하는 것이 예외적으로 허용되는 아주 드문 케이스입니다.)
기존의 map 방식을 수동으로 배열에 푸시(push)하는 루프로 바꾸거나, 아니면 Fragment를 활용해 보세요.
해설:
수동으로 루프를 돌면서 출력 배열을 만들고, 그 배열 안에 <hr />과 <p>...</p>를 차례대로 밀어 넣는(push) 방식으로 작성할 수 있습니다.
// src/App.js
const poem = {
lines: [
'I write, erase, rewrite',
'Erase again, and then',
'A poppy blooms.'
]
};
export default function Poem() {
let output = [];
// output 배열 채우기
poem.lines.forEach((line, i) => {
output.push(
<hr key={i + '-separator'} />
);
output.push(
<p key={i + '-text'}>
{line}
</p>
);
});
// 가장 첫 번째에 들어간 <hr />은 문단 사이가 아니므로 빼버립니다.
output.shift();
return (
<article>
{output}
</article>
);
}
// styles.css
body {
text-align: center;
}
p {
font-family: Georgia, serif;
font-size: 20px;
font-style: italic;
}
hr {
margin: 0 120px 0 120px;
border: 1px dashed #45c3d8;
}
원래 쓰던 대로 줄의 인덱스 번호만 달랑 key로 쓰면 이제 문제가 생깁니다. 구분선 <hr />과 문단 <p>가 같은 배열 안에 섞여버려서 서로 키값이 중복되어 버리거든요. 하지만 위 예제처럼 인덱스 뒤에 접미사를 붙여서 key={i + '-text'} 나 key={i + '-separator'} 처럼 고유한 이름을 만들어 주면 해결됩니다.
또 다른 방법으로는, <hr />과 <p>...</p>를 한 묶음으로 포함하는 Fragment를 렌더링하는 방법도 있어요. 다만 key를 전달해야 하기 때문에 <>...</> 같은 단축 문법은 쓸 수 없고, 풀네임인 <Fragment>를 명시적으로 써주셔야 합니다.
// src/App.js
import { Fragment } from 'react';
const poem = {
lines: [
'I write, erase, rewrite',
'Erase again, and then',
'A poppy blooms.'
]
};
export default function Poem() {
return (
<article>
{poem.lines.map((line, i) =>
<Fragment key={i}>
{i > 0 && <hr />}
<p>{line}</p>
</Fragment>
)}
</article>
);
}
// styles.css
body {
text-align: center;
}
p {
font-family: Georgia, serif;
font-size: 20px;
font-style: italic;
}
hr {
margin: 0 120px 0 120px;
border: 1px dashed #45c3d8;
}
잊지 마세요! Fragment (보통 <> </> 로 쓰는 그것)를 사용하면 불필요한 <div> 껍데기를 추가하지 않고도 여러 개의 JSX 노드들을 하나로 묶어줄 수 있답니다!
모든 문서 페이지 개요 (Overview of all docs pages)
수고하셨습니다! 오늘 번역해 드린 내용은 어떠셨나요? React와 Next.js 개발에서 기초이자 뼈대가 되는 배열 다루기 내용이라 조금 길었지만, 한 번 제대로 익혀두시면 현업에서 정말 요긴하게 쓰실 수 있을 거예요. 공식 문서 번역이나 부연 설명이 더 필요한 부분이 있다면 언제든 편하게 말씀해 주세요!