추석연휴 전 마지막 리액트 과제가 주어졌어요. 이 과제의 목표는 아래와 같이 크게 세 가지였답니다.
- API 호출
- 컴포넌트 재사용
- 이름 필터링
API호출과 컴포넌트 재사용은 이전에 연습했던 내용이었기에 복습해볼 수 있었어요. 하지만 필터링 부분에서 🚨경고 문구🚨를 마주하고 해결하는 과정을 통해 useEffect
에 대해 좀 더 이해 할 수 있게 됐어요.
아래는 json placeholder의 테스트 데이터의 일부랍니다. 배열 속에 다양한 user의 정보가 객체의 형태로 저장되어있는걸 알 수 있어요.
setState로 monsters
라는 state에 API를 호출해 받아온 정보를 저장해줘요. 첫 렌더링이 끝나 UI가 화면에 모두 그려진 뒤, 데이터를 불러올거기 때문에 useEffect
속에 fetch
를 넣어 불러온 데이터를 저장해줬어요.
// Monsters 컴포넌트
const [monsters, setMonsters] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json()) // JSON형태의 데이터를 자바스크립트 형태로 변환해 반환
.then((data) => setMonsters(data)); // 반환한 결과를 monsters에 저장
}, []);
컴포넌트 구조는 아래의 이미지와 같아요. Monsters컴포넌트 속 CardList컴포넌트가 있죠. 그리고 CardList에는 다양한 사용자의 정보가 담긴 Card가 똑같은 형태로 내용만 다르게 반복적으로 담겨있어요. 그래서 CardList에 map()
을 사용해 Card를 반복적으로 그려줬어요.
CardList 컴포넌트는 부모인 Monsters컴포넌트로부터 monsters
를 받아와요. monsters
에는 배열 속에 다양한 user의 정보가 객체의 형태로 담겨있죠? 각 객체를 map()
으로 돌며 Card컴포넌트에 다양한 정보를 넘겨줘요. id
, name
, email
을 넘겨주었어요. map()
을 사용할 때 key
를 설정하지 않으면 error메세지가 뜬다는 점!
// CardList 컴포넌트
function CardList({ monsters }) {
return (
<div>
{monsters.map((data) => (
<Card
key={data.id}
id={data.id}
name={data.name}
email={data.email}
/>
))}
</div>
);
}
아래는 CardList 컴포넌트 속에 있는 Card컴포넌트랍니다. 앞서 CardList 컴포넌트가 보내준 id
, name
, email
을 구조분해 할당으로 받아왔어요. 이렇게 되면 CardList로부터 받아온 id
, name
, email
의 내용에 따라 각각의 Card가 다른 내용을 담게 되겠죠?
// Card 컴포넌트
function Card({ id, name, email }) {
return (
<div>
<img
src={`https://robohash.org/${id}?set=set2&size=180x180`}
alt="몬스터 사진"
/>
<h2>{name}</h2>
<p>{email}</p>
</div>
);
}
아래 보이는 Search검색창을 통해 아래 몬스터의 이름을 필터링한 결과를 볼 수 있어요. e
를 치면 e
가 이름에 포함된 몬스터들만이 화면에 그려지죠. ervin
을 치면 아래 이미지의 왼쪽 몬스터만 나오게 되겠죠?
이를 구현하기 위해 우선 input에 사용자가 어떤 값을 입력하는지 저장해줘야해요. onChange이벤트의 콜백함수를 handleChange
로 따로 선언해줬답니다. input내용이 바뀔때마다 setUserInput
을 통해 그 내용이 저장되겠죠?!
// input의 onChange 이벤트로 사용자가 입력한 input 저장
const [userInput, setUserInput] = useState("");
const handleChange = (e) => {
setUserInput(e.target.value);
};
만약에 사용자가 input에 한 글자라도 입력하면 그 내용이 몬스터의 이름에 있는지 확인해줘야해요. 그래서 몬스터의 이름을 toLowerCase()
로 모두 소문자로 바꿔준 뒤, 사용자가 input에 입력한 값이 몬스터 이름에 들어있는지 확인해줬어요. 그리고, 만약 몬스터 이름에 해당 값이 있으면 filteredMonster
라는 새로운 배열에 저장해줬어요. 검색하면 검색결과에 해당되는 몬스터만을 화면에 그려줘야하기 때문이죠!
// 수정 전 코드
const [filteredMonster, setFilteredMonster] = useState([]);
useEffect(() => {
setFilteredMonster(
monsters.filter((v) => v.name.toLowerCase().includes(userInput))
);
}, [userInput]);
그런데 작동은 잘 됐지만 경고 메세지가 뜨더라구요. useEffect
속에 monsters
라는 state를 사용하면서 의존성 배열에는 왜 그 state가 없냐!는 경고였어요.
useEffect
내에서 사용하는 state는 의존성 배열에 넣어줘야한다는 것! 의존성 배열에 넣은 state가 변화할 때마다 첫번째 인자에 있는 내용이 실행되니까요.
// 수정 후 코드
const [filteredMonster, setFilteredMonster] = useState([]);
useEffect(() => {
setFilteredMonster(
monsters.filter((v) => v.name.toLowerCase().includes(userInput))
);
}, [monsters, userInput]);
input값을 입력하면 필터링된 정보를 갖고 있는 filteredMonster
가 그려지고, 입력하지 않았을 땐 monsters
가 화면에 그려져요. 구현에는 성공했지만 그렇게 효율적인 코드는 아니란 생각이 들어요. filteredMonster
라는 새로운 배열을 만들지 않고 해결할 수 있는 방법은 없는지 좀 더 고민해봐야겠습니다!
function CardList({ monsters, filteredMonster, userInput }) {
return (
<div>
{userInput.length > 0
? filteredMonster.map((data) => (
<Card
key={data.id}
id={data.id}
name={data.name}
email={data.email}
/>
))
: monsters.map((data) => (
<Card
key={data.id}
id={data.id}
name={data.name}
email={data.email}
/>
))}
</div>
);
}
# 리팩토링
이름 필터링처럼 구현해 작동하긴 하는데 코드가 제가 봐도 효율적이지 않다는 생각이 들 때가 많아요. 구현에서 끝내지 말고 리팩토링에도 시간을 투자해야함을 느끼는 요즘입니다!
# 이론 & 실습
이론으로 배우는 것과 직접 적용해보는거랑 정말 다르더라구요. 이론적으로 어느정도 안다고 생각했는데 직접 적용해보면 제대로 알고 있는게 아닐 때가 많아요. 실습해보며 부족한 내용을 채워나가는게 가장 기억에도 잘 남는 것 같아요. 실습하며 새롭게 알게 된 내용이나 개념은 꼭 정리하고 넘어가자는 다짐을 해봅니닷📝💪🏻