이 글은 김민준(velopert)님의 리액트를 다루는 기술을 참조하였습니다.
목차
1. JS map() function, Component array
2. key
3. Create & Delete
1. JS map() function, Component array
map 함수는 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환한다.
기본적으로 알고 있어야 굉장히 편하다.
arr.map(callback, [thisArg]);
callback 함수의 param은 현재 처리하고 있는 요소, 요소의 index 번호, 원본 배열이며
thisArg는 callback 함수 내부에서 사용할 this 레퍼런스인데 선택사항이다.
const arr = [1,3,5,7];
const map = arr.map(x => x *2);
console.log(map); // [2,6,10,14]
예를 들어, 이런 코드가 있다고 생각해보자.
const Contents = () => {
return (
<ul>
<li>html is ...</li>
<li>css is ...</li>
<li>javascript is ...</li>
</ul>
)
}
만약 이 리스트의 개수가 1억 개 있다고 가정한다면 이걸 하나하나 다 입력하고 있을 순 없다.
굳이 1억개가 아니더라도 보여주고 싶은 li 요소를 수정하려고 한다면 이런 무식한 방법으로는 관리할 수 없다.
그렇다면 map함수를 이용해서 li 태그 안에 원하는 정보를 모두 담아놓은 객체를 저장한 후에
JSX로 한 번에 렌더링 시키면 어떨까?
import { useState } from 'react'
const Contents = () => {
const [topics, setTopics] = useState([
{id: 1, title: 'html', body: 'html is ...'},
{id: 2, title: 'css', body: 'css is ...'},
{id: 3, title: 'js', body: 'javascript is ...'},
])
const list = topics.map((param) => <li>{param['body']}</li>);
return (
<ul>
{list}
</ul>
)
}
export default Contents
이렇게 하면 topics의 요소에 하나씩 접근하면서 body의 item을 가져와 li태그로 묶은 후
차례로 list 변수에 저장해 return 시킨다.
이건 굉장히 유용하고 자주 사용하는 방법이므로 기억하고 있어야 한다.
그런데 과연 여기서 끝일까? 애석하게도 그렇지 않다. 개발자 콘솔을 열어보면 이런 경고가 떠있을 것이다.
각각의 list 안에는 유니크한 "key"를 prop로써 전달해주어야 한다고 한다.
대체 key가 뭔데?
2. Key
나도 이런 이미지들을 만들어보고 싶은데 대체 어떻게 하는거지??
어쨌든 설명 100줄 적는 것보다 이미지 하나가 훨씬 도움되는 것 같아서 가져왔다.
key값을 정해주는 이유는 컴포넌트 배열을 렌더링할 때, 어떤 원소에 변동이 있었는지 확인하기 위함이다.
내가 예시로 든 topics라는 객체는 정보가 생성/제거/수정될 수 있는 유동적인 데이터다.
key 값을 정해주지 않으면 VDOM은 렌더링 하기 위해 리스트를 차례로 다 읽어가면서
변화된 사항을 알아내야 하는 반면, Key값이 있으면 보다 빠르게 작동할 수 있다.
그럼 key값으로 무엇을 던져주어야 할까? 주의할 점은 index값을 던져주어서는 안 된다는 점이다.
물론 index값은 유니크하지만 위의 이미지를 다시 확인해보자.
0~4번 인덱스까지 채워져있는 리스트에서 2번 리스트를 제거해버리면 인덱스가 재배치된다.
그 결과 변경된 index값을 재반영시키기 위해서 map함수를 사용해 list를 갱신해주어야 한다.
만약 변경하지 않고 그대로 사용하고자 한다면 key값으로 주어진 index와 실제 index값이 불일치하여
본래 key를 속성으로 주는 목적이 상실되고 렌더링 성능이 저하된다.
그렇다면 어떻게 고유값을 설정할 수 있을까?
3. Create & Delete
위에서 정의한 topics 정보를 추가 / 제거 / 업데이트 해보자.
import { useState } from 'react'
const Contents = () => {
const [topics, setTopics] = useState([ // 결과적으로 출력할 리스트
{id: 1, body: 'html is ...'},
{id: 2, body: 'css is ...'},
{id: 3, body: 'javascript is ...'},
])
const [inputText, setInputText] = useState(null); // 새로 추가할 객체의 body
const [nextId, setNextId] = useState(4); // 새로 추가할 객체의 id
const onChange = (e) => {setInputText(e.target.value)}
const list = topics.map((param) => <li key={param['id']}>{param['body']}</li>);
return (
<>
<input value={inputText} onChange={onChange} />
<button>Add</button>
<ul>{list}</ul>
</>
)
}
export default Contents
기본적인 틀은 위의 코드를 따른다.
1. Create
const onClick = (e) => {
(...)
const newTopics = topics.concat({
id: nextId,
body: inputText
});
setNextId(nextId + 1); // 다음에 추가될 topics를 위해 1 증가
setTopics(newTopics); // topics 목록에 새롭게 입력된 정보 추가
setInputText(''); // 버퍼를 비움으로써 input 입력값을 제거한다.
}
(...)
return (
<>
<input value={inputText} onChange={onChange} />
<button onClick={onClick}>Add</button>
<ul>{list}</ul>
</>
)
}
button을 클릭했을 때, input 태그에 담겨진 정보와 nextId를 값으로 갖는 newTopic 객체를 만들어
기존의 Topics 객체에 추가해주면 된다.
input 태그에 "hello?"를 누르고 버튼을 입력하니 id값과 함께 객체에 저장되었음을 확인할 수 있다.
물론 inputText가 비어있는 경우를 예외처리 해주어야 하지만 지금은 완벽한 구현이 목적이 아니므로 고려하지 않겠다.
한 가지 내용을 덧붙이자면, 이번에는 concat 함수를 사용했지만 나는 원래 다음과 같은 방법을 더 선호한다.
const onClick = (e) => {
const newTopic = {id:nextId, body:inputText}
const newTopics = [...topics, newTopic]
setNextId(nextId + 1); // 다음에 추가될 topics를 위해 1 증가
setTopics(newTopics); // topics 목록에 새롭게 입력된 정보 추가
setInputText(''); // 버퍼를 비움으로써 input 입력값을 제거한다.
}
어느 방법이든 편한 걸 골라서 하면 된다.
2. Delete
지우고 싶은 항목을 더블 클릭하면 해당 Topic의 id값 정보를 받아서 배열에서 삭제할 것이다.
const onRemove = (id) => {
const newTopics = topics.filter((topic) => topic.id !== id);
setTopics(newTopics);
}
const list = topics.map((param) =>
<li key={param['id']}
onDoubleClick={()=>onRemove(param['id'])}>{param['body']}</li>);
li 태그를 만들 때 더블 클릭 이벤트를 추가해준다. 이벤트가 발생하면 선택된 리스트의 id가 함수 인자로 전달되고,
onRemove함수는 id를 매개변수로 삼아 Topics 배열을 재구성한다.
Update와 Read까지 구현해서 화면에 출력하는 내용도 조정해보려고 했지만 뒤에 더 정리해야 할 내용이 있으므로
시간관계상 이번엔 여기까지만 구현하고 다음 포스팅으로 넘어가야겠다.
+전체 코드
import { useState } from 'react'
const Contents = () => {
const [topics, setTopics] = useState([ // 결과적으로 출력할 리스트
{id: 1, body: 'html is ...'},
{id: 2, body: 'css is ...'},
{id: 3, body: 'javascript is ...'},
])
const [inputText, setInputText] = useState(null); // 새로 추가할 객체의 body
const [nextId, setNextId] = useState(4); // 새로 추가할 객체의 id
const onChange = (e) => {setInputText(e.target.value)}
const onClick = (e) => {
const newTopic = {id:nextId, body:inputText}
const newTopics = [...topics, newTopic]
setNextId(nextId + 1); // 다음에 추가될 topics를 위해 1 증가
setTopics(newTopics); // topics 목록에 새롭게 입력된 정보 추가
setInputText(''); // 버퍼를 비움으로써 input 입력값을 제거한다.
}
const onRemove = (id) => {
const newTopics = topics.filter((topic) => topic.id !== id);
setTopics(newTopics);
}
const list = topics.map((param) => <li key={param['id']} onDoubleClick={()=>onRemove(param['id'])}>{param['body']}</li>);
return (
<>
<input value={inputText} onChange={onChange} />
<button onClick={onClick}>Add</button>
<ul>{list}</ul>
</>
)
}
export default Contents