useState와 useEffect로 React의 상태와 생명주기를 관리하고, MUI·styled-components로 재사용 가능한 버튼을 만든 뒤 Axios 공통 인스턴스로 서버 데이터를 요청하는 방법을 학습했다.
Hook은 함수형 컴포넌트에서 상태와 생명주기 등의 React 기능을 사용할 수 있도록 제공하는 함수다.
import {
useState,
useEffect,
} from "react";
대표적인 Hook으로 다음 두 가지가 있다.
useState: 컴포넌트의 상태를 관리한다.useEffect: 렌더링 이후 실행할 작업을 관리한다.Hook은 컴포넌트의 최상위 영역에서 호출해야 한다. 조건문이나 반복문 내부에서 호출하면 안 된다.
일반 변수의 값을 변경해도 React는 화면을 다시 렌더링하지 않는다.
let count = 0;
const increase = () => {
count += 1;
};
화면과 함께 변경돼야 하는 값은 state로 관리해야 한다.
const [count, setCount] = useState(0);
useState(0)은 두 값을 반환한다.
count: 현재 상태setCount: 상태를 변경하는 함수setCount() 호출
↓
state 변경
↓
컴포넌트 다시 렌더링
↓
변경된 값이 화면에 표시
기존 상태를 기준으로 새로운 값을 계산할 때는 함수형 업데이트를 사용할 수 있다.
const increaseHandler = () => {
setCount((previousCount) => previousCount + 1);
};
const decreaseHandler = () => {
setCount((previousCount) => previousCount - 1);
};
다음 코드보다 안전한 방법이다.
setCount(count + 1);
React는 상태 변경을 모아서 처리할 수 있기 때문에 이전 상태를 기반으로 계산할 때는 함수형 업데이트를 사용하는 것이 좋다.
상태 변경 함수는 현재 실행 중인 함수의 변수를 즉시 변경하지 않는다.
const increaseHandler = () => {
setCount((previousCount) => previousCount + 1);
console.log(count);
};
위 로그에는 변경되기 전 값이 출력될 수 있다. 현재 이벤트 핸들러가 실행되는 동안의 count는 기존 렌더링 시점의 값이기 때문이다.
변경된 값을 확인하려면 useEffect를 사용할 수 있다.
useEffect(() => {
console.log(`변경된 인원: ${count}`);
}, [count]);
useEffect는 컴포넌트가 렌더링된 이후 실행해야 하는 작업을 처리한다.
useEffect(() => {
console.log("컴포넌트 렌더링 이후 실행");
});
대표적인 사용 사례는 다음과 같다.
두 번째 인수인 의존성 배열에 따라 실행 시점이 달라진다.
// 렌더링될 때마다 실행
useEffect(() => {
console.log("렌더링");
});
// 컴포넌트가 처음 렌더링된 후 한 번 실행
useEffect(() => {
console.log("최초 실행");
}, []);
// count가 변경될 때 실행
useEffect(() => {
console.log(count);
}, [count]);
현재 인원 변경을 감지하려면 빈 배열이 아닌 [count]를 전달해야 한다.
CapacityPage에서 입장 인원을 state로 관리했다.
const CapacityPage = () => {
const [count, setCount] = useState(0);
const increaseHandler = () => {
setCount((previousCount) => previousCount + 1);
};
const decreaseHandler = () => {
setCount((previousCount) => previousCount - 1);
};
useEffect(() => {
console.log(`현재 인원: ${count}`);
}, [count]);
return (
<div>
<p>입장 인원: {count}</p>
<Button
title="입장"
onClick={increaseHandler}
/>
<Button
title="퇴장"
onClick={decreaseHandler}
/>
</div>
);
};
count가 변경될 때마다 컴포넌트가 다시 렌더링되어 화면의 입장 인원도 변경된다.
퇴장 인원이 음수가 되지 않게 하려면 조건을 추가할 수 있다.
const decreaseHandler = () => {
setCount((previousCount) =>
Math.max(0, previousCount - 1),
);
};
부모 컴포넌트에서 만든 이벤트 함수를 자식 컴포넌트에 Props로 전달할 수 있다.
<MaterialButton
title="글 작성하기"
onClick={saveHandler}
/>
자식 컴포넌트는 전달받은 함수를 버튼 이벤트에 연결한다.
const MaterialButton = ({
title,
onClick,
}) => {
return (
<Button onClick={onClick}>
{title}
</Button>
);
};
다음처럼 불필요하게 함수를 한 번 더 감싸지 않아도 된다.
// 동작하지만 불필요하게 감싼 형태
onClick={() => saveHandler()}
// 함수를 바로 전달
onClick={saveHandler}
이벤트 처리 로직은 부모가 관리하고, 자식 버튼은 화면과 클릭 연결에 집중할 수 있다.
MUI는 React에서 사용할 수 있는 UI 컴포넌트 라이브러리다.
현재 프로젝트에는 다음 패키지가 설치되어 있다.
npm install @mui/material @emotion/react @emotion/styled
MUI의 버튼을 가져와 사용할 수 있다.
import Button from "@mui/material/Button";
const MaterialButton = ({
title,
onClick,
}) => {
return (
<Button
variant="contained"
onClick={onClick}
>
{title}
</Button>
);
};
MUI를 사용하면 버튼, 입력창, 모달 등의 UI를 직접 처음부터 만들지 않고 재사용할 수 있다.
styled-components는 JavaScript 파일 안에서 CSS를 작성하는 CSS-in-JS 라이브러리다.
import styled from "styled-components";
const StyledButton = styled.button`
padding: 8px 16px;
font-size: 16px;
border-radius: 8px;
cursor: pointer;
`;
만든 스타일 컴포넌트는 일반 React 컴포넌트처럼 사용한다.
const Button = ({
title,
onClick,
}) => {
return (
<StyledButton onClick={onClick}>
{title}
</StyledButton>
);
};
외부 CSS 파일 없이 컴포넌트와 스타일을 하나의 파일에서 관리할 수 있다.
MUI
→ 미리 만들어진 디자인 시스템 사용
styled-components
→ 필요한 디자인을 직접 컴포넌트로 작성
Axios 요청을 보낼 때마다 서버 주소를 반복해서 작성하지 않도록 공통 인스턴스를 만들 수 있다.
import axios from "axios";
const endPoint =
process.env.REACT_APP_BACKEND_ENDPOINT;
const api = axios.create({
baseURL: endPoint,
});
export default api;
페이지에서는 공통 인스턴스를 가져와 상대 경로만 작성한다.
import api from "../../api/axios";
const response = await api.get("/comment");
환경변수의 서버 주소
+
/comment
↓
최종 요청 URL
공통 인스턴스를 사용하면 다음 설정을 한곳에서 관리할 수 있다.
baseURLCreate React App에서 브라우저 코드로 사용할 환경변수는 REACT_APP_으로 시작해야 한다.
REACT_APP_BACKEND_ENDPOINT=http://localhost:3001
JavaScript에서는 다음과 같이 사용한다.
process.env.REACT_APP_BACKEND_ENDPOINT
환경변수를 변경하면 개발 서버를 다시 실행해야 적용된다.
프론트엔드 환경변수는 브라우저 번들에 포함될 수 있으므로 비밀번호나 API 비밀 키를 저장하면 안 된다.
일반 변수에 Axios 응답을 할당해도 화면이 자동으로 다시 렌더링되지 않는다.
let comments = [];
const loadData = async () => {
const response = await api.get("/comment");
comments = response.data;
};
서버 응답을 화면에 표시하려면 state로 관리해야 한다.
const [comments, setComments] =
useState([]);
컴포넌트가 처음 렌더링됐을 때 데이터를 요청한다.
useEffect(() => {
const loadComments = async () => {
try {
const response =
await api.get("/comment");
setComments(response.data);
} catch (error) {
console.error(error);
}
};
loadComments();
}, []);
state가 변경되면 댓글 목록도 다시 렌더링된다.
return (
<div>
{comments.map((comment, index) => (
<Comment
key={`${comment.writer}-${index}`}
data={comment}
/>
))}
</div>
);
가능하면 서버 데이터에 고유한 id를 추가하고 이를 key로 사용하는 것이 좋다.
db.json에 댓글 목록을 저장해 로컬 API 데이터처럼 사용할 수 있다.
{
"comment": [
{
"writer": "hy",
"comment": "hihi"
}
]
}
이 데이터를 API로 제공하면 Axios에서 다음 경로로 요청할 수 있다.
api.get("/comment");
백엔드 서버가 준비되지 않은 상황에서도 프론트엔드의 API 요청과 렌더링을 연습할 수 있다.
useState로 관리해야 한다.useEffect의 의존성 배열에 따라 실행 시점이 달라진다.setCount() 호출 직후 로그에서 이전 값이 출력되는 이유가 헷갈렸다.useEffect(() => {}, [])와 useEffect(() => {}, [count])의 차이가 헷갈렸다.useState로 입장 인원을 관리했다.useEffect로 컴포넌트 렌더링 이후의 작업을 확인했다.Button을 감싸 재사용 가능한 버튼 컴포넌트를 만들었다.baseURL을 설정했다.일반 변수가 아닌 state를 사용해 값의 변경과 화면 렌더링을 연결할 수 있었다. 또한 서로 다른 UI 라이브러리로 공통 버튼을 만들고, 공통 Axios 인스턴스를 통해 API를 호출하는 구조를 이해했다.
입장·퇴장 버튼을 눌러 일반 변수를 변경해도 화면이 갱신되지 않았다. setCount() 직후 로그에는 이전 값이 출력됐으며, 댓글 API 응답을 일반 변수에 저장했을 때도 목록이 다시 렌더링되지 않았다.
화면에 영향을 주는 값은 useState로 관리하고 setter 함수로 변경했다.
setCount(
(previousCount) => previousCount + 1,
);
변경된 상태를 확인해야 할 때는 해당 값을 useEffect의 의존성 배열에 추가했다.
useEffect(() => {
console.log(count);
}, [count]);
Axios 응답도 state에 저장해 컴포넌트가 다시 렌더링되도록 구성했다.
const response =
await api.get("/comment");
setComments(response.data);
목록을 렌더링할 때는 각 컴포넌트에 key를 지정했다.
useState에 저장하기