새싹 수업을 듣다가 놓친 부분이 있어 코딩온 온라인 강의를 들으며 복습한 내용을 정리해보려 한다.
리액트에서 중요한 개념은 state, props, 그리고 Life Cycle이라고 한다. Life Cycle은 말 그대로 생명주기를 말한다. 컴포넌트에 웬 생명을 논하는가 싶을 텐데, 프로그래밍 언어도 결국 사람이 만든 것이라 우리네 삶을 본뜬 개념이 많은 듯하다.
태어나 살아가면서 변화를 겪다 죽는 인간들처럼 컴포넌트 각각도 처음 렌더링되고, 데이터를 처리하며 수정되다가 때로는 화면에서 사라진다. 각각을 명칭으로 정리하면 아래와 같다.
Mount : DOM 객체 생성되고 브라우저에 렌더링
Update : state나 props의 변경, 부모 컴포넌트의 변경(리렌더링)
Unmount : 컴포넌트가 화면에서 제거
리액트는 달라진 부분만 자동으로 리렌더링하는 게 핵심 중 하나다. '변화'는 이전과 달라진 모든 걸 뜻한다. mount(전에 없던 것이 생겨남), update(바뀜), unmount(있던 것이 사라짐) 모두 변화를 상징한다.
특정한 때에 특정 동작을 수행할 수 있도록 코드를 짤 수 있도록 하다 보니 이런 개념을 만든 게 아닐까 싶었다.
사실 Life Cycle은 컴포넌트의 주기를 구분하는 것이지, 그자체로 무언가 사용하는 건 아니다. 리액트에서 제공하는 내장 함수인 hook을 사용한다.
클래스형 컴포넌트에만 사용할 수 있던 Life Cycle이 리액트 16.8 버전부터는 함수형 컴포넌트로 확장되었다. 이게 가능한 건 hook 덕분이다. 리액트는 'use'를 접두어로 하는 여러 가지 훅이 존재하는데, 그중에서 useEffect
가 바로 Life Cycle 개념을 접목해서 쓸 수 있는 함수이다.
useEffect(콜백 함수, 의존성 배열);
사용 방법은 간단하다.
실행할 함수를 콜백 함수로 정의하고, 어떤 컴포넌트와 연결지을지 의존성 배열을 작성한다. useEffect는 컴포넌트가 렌더링 된 후 실행되어서 mount될 때 반드시 동작하는데, if문 등을 활용해 조건을 걸어주면 업데이트 될 때만 동작하도록 만들 수 있다.
예를 들어 counter 기능을 만든다고 하자.
import { useState, useEffect } from 'react';
function Counter() {
const [number, setNumber] = useState(0);
const handleOnclick = () => {
setNumber(number + 1);
}
useEffect(() => {
if(number > 0) {
// number가 업데이트 될 때에만 동작
}, [number])
return (
<>
<div>{number}</div>
<button onClick={handleOnClick}>더하기</button>
</>
);
}
export default Counter;
if(number > 0)
을 작성한 것처럼 업데이트 된 화면 상태를 어떻게 특정할 수 있을지를 생각하면 된다. 그리고 의존성 배열에 컴포넌트에서 업데이트가 일어날 부분을 작성했다. 의존성 배열이 콜백함수의 호출 시점을 결정하기 떄문이다.
의존성 배열을 작성하지 않거나 빈 배열 :
임의의 컴포넌트가 리렌더링될 때마다 콜백함수 호출
➡️ 컴포넌트의 mount, update
의존성 배열에 특정 컴포넌트(들) 작성 :
해당 컴포넌트가 리렌더링 될 때마다 콜백함수 호출
➡️ 특정 컴포넌트의 mount, update
그럼 컴포넌트가 제거(unmount) 될 때 동작할 함수는 어디에 작성할까? 바로 콜백함수 내부의 return문으로 작성한다. 여러 개념이 우수수 쏟아졌을 땐 직접 구현해 보는 게 이해하기 가장 좋은 것 같다.
제공된 예제를 풀어보자! 💪
먼저 useEffect 부분만 보자.
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts/'
);
const getFakePosts = response.data;
setTimeout(() => {
setPost(getFakePosts.slice(0, 10));
}, 2000);
} catch (err) {
console.error('fetch Data Err :', err);
}
};
fetchData();
}, []);
try catch문으로 API를 제대로 받아오지 못한 에러 상황에 대비한다.
useEffect의 콜백함수는 직접적으로 async/await을 사용할 수 없다. 그래서 함수를 내부에 만들고, 호출하는 방식으로 작성했다.
특정 컴포넌트를 팔로잉하는 동작이 아니라서 의존성 배열은 비어뒀다. 아예 생략해도 되지만, 프로젝트할 땐 컴포넌트가 훨씬 많을 테니 의존성 배열은 빼먹지 않고 작성하는 게 좋을 거 같다.
axios 요청이니까 비동기 처리!
setTime
도 마찬가지로 비동기 처리하는 함수라서 콜백 함수를 첫번째 인자로 넘긴다. 받아온 데이터 양이 너무 많아서 slice
로 10개까지만 화면에 보여준다.
전체 코드를 보면 이렇다.
function PostList() {
const [post, setPost] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts/'
);
const getFakePosts = response.data;
setTimeout(() => {
setPost(getFakePosts.slice(0, 10));
}, 2000);
} catch (err) {
console.error('fetch Data Err :', err);
}
};
fetchData();
}, []);
return (
<div className={styles.boxContainer}>
{post.length > 0 ? (
post.map((val) => (
<div key={val.id} className={styles.box}>
<div className={styles.boxTitle}>
No. {val.id} - {val.title}
</div>
<div>{val.body}</div>
<br />
</div>
))
) : (
<h1>Loading...</h1>
)}
</div>
);
}
export default PostList;
2초가 되기 전(포스트를 받아오기 전)에 아무 문구도 보여주지 않는다면 에러처럼 보일 수 있다. 그래서 post.length
로 예외 처리를 한다.
module.css 연습 겸 CSS 중복 방지를 해주었다.
CSS 얘기를 한 김에 반응형 이야기도 살짝 얹어본다.
다양한 디바이스 크기가 나오는 요즘. 특히나 모바일은 기기별로 조금씩 크기가 다른데 이걸 일대일 대응해주는 건 무리이다. 그래서 단위를 vw
, vh
로 작성하면 스케일 단위로 움직이기 때문에 대응하기 적절하다고 한다.
@media (max-width: 768px) {
.PostItem {
width: 90vw;
}
}
위 예제에서는 unmount를 사용해 보기 적절하지 않은 듯하여 살짝 변형해봤다.
이번에도 useEffect 부분부터 살펴보자.
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts/'
);
const getFakePosts = response.data;
setPost(getFakePosts.slice(0, 10));
} catch (err) {
console.error('fetch Data Err :', err);
}
};
fetchData();
return () => {
setLoading(true);
};
}, [isMount]);
전체 코드를 보자면,
import { useState, useEffect } from 'react';
import axios from 'axios';
import styles from '../../prac1.module.css';
function PostList() {
const [post, setPost] = useState([]);
const [isMount, setIsMount] = useState(true);
const [loading, setLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(
'https://jsonplaceholder.typicode.com/posts/'
);
const getFakePosts = response.data;
setPost(getFakePosts.slice(0, 10));
} catch (err) {
console.error('fetch Data Err :', err);
}
};
fetchData();
return () => {
setLoading(true);
};
}, [isMount]);
const toggleMount = () => setIsMount(!isMount);
return (
<div className={styles.wrapper}>
{isMount ? (
<>
<div>
<button onClick={toggleMount}>포스트 제거</button>
</div>
<div className={styles.boxContainer}>
{post.map((val) => (
<div key={val.title} className={styles.box}>
<div className={styles.boxTitle}>
No. {val.id} - {val.title}
</div>
<div>{val.body}</div>
<br />
</div>
))}
</div>
</>
) : (
<>
<button onClick={toggleMount}>포스트 생성</button>
{loading ? <h1>Loading...</h1> : ''}
</>
)}
</div>
);
}
export default PostList;
UI가 달라졌기 떄문에 컴포넌트가 return하는 요소들에 큰 변화가 생겼다.
우선 컴포넌트가 마운트 되었음을 확인하는 isMount
로 어떤 화면을 보여줄지 나누었다.
isMount
, loading
모두 토글이라서 이름만 다를 뿐이지 사실상 똑같은 역할이다. 그래서 변수명만 바꿔서 하나의 state로 통합하는 게 더 효율적이라고 본다. 또, 자주 쓰일 건 Custom hook으로 만들어 봐도 좋겠다.
🌱 블로깅 할 때마다 느끼지만 내가 얼마나 알고 있는지, 얼마나 배웠는지도 한번 되돌아 볼 수 있어서 좋다. 게다가 코드를 다시 읽다보니 개선할 점도 덩달아 보인다.
🌱 데이터 변동이 많은 UI를 다룰 때 리액트가 유리하단 것에서 트위터나 에어비앤비, 인스타그램 등 SNS 플랫폼이 활용하는 이유를 이해했다. 그런데 SPA라서 SEO가 거의 불가하다는 것 또한 상당한 영향이었을 것이다.
🌱 트위터나 인스타그램이야 앱 내부 검색 기능이 중요하지, 구글에 웹사이트가 검색되고 말고는 크게 상관할 바가 아니니까 전혀 제약이 되지 않는다. 에어비앤비도 마찬가지이고. 하지만 검색 엔진에 걸려야 하거나 웹사이트 기반 비즈니스에겐 치명타가 아닐 수 없다.
useEffect는 여전히 알다가도 모르겠지만 블로그 작성하면 할수록 정리가 되는 건 크게 공감합니다. 👍