오늘은 상태 끌어올리기와 Effect Hook 사용법을 알고 컴포넌트 내 Ajax 호출까지 배워보자!
알아야 할 것: state이 아닌 것
- 부모로부터 props를 통해 전달되나?
- 시간이 지나도 변하지 않나?
- 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가?
상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행하는 스킬
props(props={Value})를 이용하여 콜백처럼 사용하자!
여기 이 문제가 있다. 한번 풀어보자! 부모는 Twittler, 자식은 NewTweetForm 등등이 있다.
import React, { useState } from "react";
import "./styles.css";
const currentUser = "김코딩";
function Twittler() {
const [tweets, setTweets] = useState([
{
uuid: 1,
writer: "김코딩",
date: "2020-10-10",
content: "안녕 리액트"
},
{
uuid: 2,
writer: "박해커",
date: "2020-10-12",
content: "좋아 코드스테이츠!"
}
]);
const addNewTweet = (newTweet) => {
setTweets([...tweets, newTweet]);
}; // 이 상태 변경 함수가 NewTweetForm에 의해 실행
return (
<div>
<div>작성자: {currentUser}</div>
<NewTweetForm />
<ul id="tweets">
{tweets.map((t) => (
<SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
{t.content}
</SingleTweet>
))}
</ul>
</div>
);
}
function NewTweetForm({ onButtonClick }) {
const [newTweetContent, setNewTweetContent] = useState("");
const onTextChange = (e) => {
setNewTweetContent(e.target.value);
};
const onClickSubmit = () => {
let newTweet = {
uuid: Math.floor(Math.random() * 10000),
writer: currentUser,
date: new Date().toISOString().substring(0, 10),
content: newTweetContent
};
// TDOO: 여기서 newTweet이 addNewTweet에 전달
};
return (
<div id="writing-area">
<textarea id="new-tweet-content" onChange={onTextChange}></textarea>
<button id="submit-new-tweet" onClick={onClickSubmit}>
새 글 쓰기
</button>
</div>
);
}
function SingleTweet({ writer, date, children }) {
return (
<li className="tweet">
<div className="writer">{writer}</div>
<div className="date">{date}</div>
<div>{children}</div>
</li>
);
}
export default Twittler;
위에서 설명한 것처럼 자식과 부모간 역방향으로 데이터를 주기에 그렇게 조작해주자
1. 부모: return에 넣어 자식을 렌더링하는데 자식의 콩고물(파라미터)를 쓰고 원하는 곳에 대입
function Twittler() {
return (
<div>
<div>작성자: {currentUser}</div>
{/* NewTweetForm의 props(파라미터) onButtonClick을 addNewTweet에 적용시켜줌 */}
<NewTweetForm onButtonClick={addNewTweet} />
<ul id="tweets">
{tweets.map((t) => (
<SingleTweet key={t.uuid} writer={t.writer} date={t.date}>
{t.content}
</SingleTweet>
))}
</ul>
</div>
);
}
function NewTweetForm({ onButtonClick }) {
const onClickSubmit = ({}) => {
let newTweet = {
uuid: Math.floor(Math.random() * 10000),
writer: currentUser,
date: new Date().toISOString().substring(0, 10),
content: newTweetContent
};
// TDOO: 여기서 newTweet이 addNewTweet에 전달
// NewTweetForm의 파라미터 onButtonClick을 가져와 newTweet을 삽입
onButtonClick(newTweet);
};
};
함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우(부수효과)를 뜻함
const obj = {value: 12};
function sum(obj, num){
return {value: obj.value + num};
}
이 코드는 함수가 실행되도 기존 obj나 obj.value에게 영향을 안끼쳐서 순수 함수임
let c = 12;
function sum(a, b){
c = a * b;
return a + b;
}
얘는 함수가 실행되면 될 수록 변수 c에 영향을 줌 그래서 순수 함수가 아님
렌더링될 때 함수를 실행하게 만들어주는 Hook
이걸 이용해 side effect를 실행할 수 있다!?
useEffect(()=>{}, [실행 조건(옵션)1, 2, ...])
서버에서부터 가져오는 fetch와 useEffect를 섞어 어떤 이벤트를 발생하면 그때 서버에서 데이터를 가져오는 기술도 쓸 수 있음!
위의 파일들은 이제 하드코딩으로 가져왔지만 만약 서버에서 수십만개의 명언을 요청한다면?
// 명언을 제공하는 API의 엔드포인트가 http://서버주소/proverbs 라고 가정
useEffect(() => {
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
});
}, [filter]);
서버에서 가져올 때 로딩 이미지를 붙여줘 가져오고있다는 표시를 하면 EX적인 부분에서 만족을 줄 수 있다!
// 로딩을 설정할 State 구현
const [isLoading, setIsLoading] = useState(true);
// 생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정합니다
return {isLoading ? <LoadingIndicator /> : <div>로딩 완료 화면</div>}
// 위 코드와 조합하여 filter의 값이 바뀔 때 마다 로딩과 fetch로 서버에서 가져옴
useEffect(() => {
setIsLoading(true);
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
setIsLoading(false);
});
}, [filter]);
이제 과제를 통해 상태 끌올, useEffect를 활용하자!
.디렉토리 구조
├── README.md
├── __tests__
│ └── index.test.js # 테스트 파일
├── api
│ └── FlightDataApi.js # 항공편 정보를 받아오는 API
├── package.json
├── pages
│ ├── Main.js # 첫 화면 컴포넌트, 필터링 상태를 담고 있습니다.
│ ├── component
│ ├── Debug.js # 디버그용 컴포넌트 (테스트 통과에 필요합니다)
│ ├── Flight.js # 단일 항공편
│ ├── FlightList.js # 항공편 목록
│ ├── LoadingIndicator.js # 로딩 컴포넌트
│ └── Search.js # 검색 도구 컴포넌트, 필터링 상태를 변경합니다.
├── public
├── resource
│ └── flightList.js # 하드코딩된 항공편 정보
└── styles
└── globals.css # 스타일 시트
Main.js와 Search.js 간 연결하기
1. condition은 출발지와 도착지를 담을 수 있음
2. search 함수는 condition을 변경하는 함수
3. 자식인 <Search />컴포넌트의 input 입력값을 부모인 Main.js의 search 함수와 연결시키는 구조
// 모듈 중략
export default function Main() {
// 항공편 검색 조건을 담고 있는 상태: useState 중략
// 주어진 검색 키워드에 따라 condition 상태를 변경시켜주는 함수
const search = ({ departure, destination }) => {
if (condition.departure !== departure || condition.destination !== destination) {
console.log("condition 상태를 변경시킵니다");
// search 함수가 전달 받아온 '항공편 검색 조건' 인자를 condition 상태에 적절하게 담아보자
setCondition({ departure, destination });
}
};
// filterByCondition(): 필터해주는 함수 중략
// search 함수를 Search 컴포넌트로 내려주기
return (
// head 부분 중략
<main>
<h1>여행가고 싶을 땐, States Airline</h1>
// Search 컴포넌트를 props로 search함수를 고정함
<Search onSearch={search} />
<div className="table">
<div className="row-header">
<div className="col">출발</div>
<div className="col">도착</div>
<div className="col">출발 시각</div>
<div className="col">도착 시각</div>
<div className="col"></div>
// 중략
// import 생략
// 파라미터 설정
function Search({ onSearch }) {
// input 설정 함수 생략
const handleSearchClick = () => {
console.log("검색 버튼을 누르거나, 엔터를 치면 search 함수가 실행");
// 상위 컴포넌트에서 props를 받아서 실행시켜 보자.
// Search의 파라미터를 가져와줌
onSearch({ departure: "ICN", destination: textDestination });
};
// return 및 export 생략
아~ 잘 짝지어줄려면 부모와 자식 관계를 명확히 알아야하고 부모에게 제대로 props를 설정, 자식도 받아주는 파라미터를 확실히 설정해서 이어주는거구나~
상태를 끌올해서 condition
이 바뀌면 검색 조건도 바뀌게 설정됐다!
filterByCondition()
를 빼고 api를 불러와 필터하게 만드는, flight
라는 params와 그 뒤 ?=query를 이용한, api/FlightDataApi.js
에 담긴 getFlight(condition)
함수를 만들자.getFlight()
를 이용해서 만들자!api/FlightDataApi.js
// 파라미터에 '='은 디폴트 파라미터로 입력되지 않았을 시 입력 되게끔, 오류가 안나게끔
export function getFlight(filterBy = {}) {
// API를 사용하기 위해, fetch를 이용. 아래 구현은 getFlight(filterBy = {})을 하드코딩한 것
// TODO: 아래 구현을 REST API 호출로 대체
return fetch(
`http://ec2-13-124-90-231.ap-northeast-2.compute.amazonaws.com:81/flight?departure=ICN&destination=${filterBy.destination}`
).then((response) => response.json());
// getFlight() 하드코딩 주석 처리 및 중략
}
filterBy
를 써서 그 조건의 데이터를 받아와 Ajax요청을 해 JSON 파일을 받아옴~compute.amazonaws.com:81/flight?
인 API를 GET하면 데이터가 날라옴flight?
뒤에 query인 조건들을 붙여 데이터를 가져옴: 필터되는 것임!!departure
는 ICN으로 고정, destination
은 Props로 받아와 condition
에 입력된 것을 가져오기(State).json()
은 fetch의 응답 메소드이며 fetch는 기본적으로 Promise타입 객체(JSON)를 반환하기에 response.json()
을 해줘 JS 객체로 쓰일 수 있게끔 설정.json()
의 반환값은 사실 JS 객체로 나오지 않는다!미친 포인트를 잡아버렸다.
이상한 점이 있었는데 .json()
은 JS 객체로 반환해준다고 했는데 위 사진을 보면 결국 Promise를 반환하는 청개구리짓을 한다. 이 새끼 왜이래?!
이거에 대한 답은 Promise의 작동 방식에 있다. fetch를 한 후에 날아오는 response 객체는 모든 header가 도착하자마자 우리에게 주어진다. 즉, header만 올 뿐 body가 아직 오지 않는다는 의미다. 그래서 그다음 .then~console.log
같은 then 체이닝 등으로 비동기 과정을 하나 더 거쳐야 JS 객체가 드디어 온다.
즉! fetch.json()
을 입력하면 해당 body 값을 '기다리고 있는 상태'인 Promise 객체를 반환-'리턴'한다!
import { useEffect, useState } from "react";
import { getFlight } from "../api/FlightDataApi";
export default function Main() {
const [condition, setCondition] = useState({
departure: "ICN",
});
const [flightList, setFlightList] = useState(getFlight);
// search 함수, filterByCondition 함수 중략
useEffect(async () => {
setFlightList(await getFlight(condition));
}, [condition]);
return (
// 중략
<Search onSearch={search} />
{/* <FlightList list={flightList.filter(filterByCondition)} /> */}
<FlightList list={flightList} />
condition
(Dependency Array, 실행 조건)가 변경되면 State 변경 함수인 setFlightList()
에 FlightDataApi파일을 import해온 getFlight()
함수를 가져왔고, condition
을 넣어 걸러지게끔 설정했다!async
, await
등 비동기를 왜 넣었을까? 왜냐면 위 참고에서 봤듯이 fetch에 .json
을 바로 해버린 상태면 Promise 객체가 나오고, useEffect와 setFlightList
는 Promise를 받지 못 async
, await
로 JS 객체를 제대로 반환 받아야 setFlightList
에 적용할 수 있다.<FlightList list={flightList} />
을 넣어 Props도 적용케 했다.로딩 기능도 넣어주자 EX를 향상시키기 위해!
당연히 gif 이미지도 추가돼있음
function LoadingIndicator() {
return (
<img
className="loading-indicator"
alt="now loading..."
src="loading.gif"
style={{ margin: '1rem' }}
/>
);
}
export default LoadingIndicator;
setIsLoading
를 어디에 배치? 렌더링 되면 로딩이 끝나게!import LoadingIndicator from "./component/LoadingIndicator";
export default function Main() {
// 상태 부분: 기본값 true
const [isLoading, setIsLoading] = useState(true);
// useEffect는 렌더링을 해주는 기능이니 useEffect의 순서에 따라 isLoading가 true, false로
useEffect(async () => {
setIsLoading(true);
setFlightList(await getFlight(condition));
setIsLoading(false);
}, [condition]);
// 조건문을 통해서 true면 로딩이 렌더링, false면 none되게
return (
// 중략
{/* <FlightList list={flightList.filter(filterByCondition)} /> */}
{/* <FlightList list={flightList} /> */}
{isLoading ? <LoadingIndicator /> : <FlightList list={flightList} />}
핵심: return으로 렌더링 설정해주는 코드가 있는데 여기에선 무조건 삼항 연산자가 必, 그리고 로딩 상태는 기본값으로 true가 좋다.(왜인지는 모르겠다.. stackoverflow에 안나온다..), useEffect와 같이 쓰이니 알아두면 너무 좋다~