리액트를 시작하면서
화면을 구성하는 부분들을 컴포넌트 단위로 나눠서 작성하는 것에 대해 배웠다.
팀프로젝트를 하면서도 느꼈던 부분이고 다른 팀 프로젝트의 코드를 살펴보면
스타일에 대해 작성한 코드와 로직을 작성하는 코드가 나눠져있는 경우를 볼 수 있었다.
CSS-in-JS 방식으로 스타일만 작성한 코드를
로직 컴포넌트에 import 해와서 랩핑해주면서 사용하는 걸 처음 봤을 때는
이걸 뭐라고 부르지? 이렇게도 쓸 수 있구나 하고 놀랐던 기억이 난다.
사실은 아직도 이런 방법에 대한 어느 정도 공식적인 명칭이 있는 건지 잘 모르겠다.
그리고 계속 원티드 챌린지와 이런 저런 공부를 해오고 프론트엔드 개발자들의 강연 발표를 보게 되면서
컴포넌트를 작성할 때 어떤 것들을 고려해야 하는지, 상황에 따른 추상화에 대한 정도와 필요성,
Hoc와 Headless component 등 여러가지 패턴에 대한 명칭과 이해가 많이 부족하다는 걸 느꼈다.
그리고 마침 이번 6월 원티드 챌린지 주제가 컴포넌트에 대한 것이었고,
강사분께서 디자인 패턴에 참고가 될 레퍼런스를 알려주셔서 정리해가며 공부하기로 했다.
부족하다고 생각하는 영역에서
정립된 명칭과 그에 대한 이해를 하고 있지 않다면,
현업에 가서도 원활한 커뮤니케이션에 어려움을 겪을 수 있기 때문에
이번 기회로 패턴에 대한 이해를 높여보고자 한다.
- Container 컴포넌트
어떤 데이터가 보여질 지에 다루는 컴포넌트
사용자에게 보여질 데이터에 관심을 둔다.
container 라는 용어로부터 컨테이너 박스라는 단어와 연관된 이미지가 연상되는데
무언가를 담는 역할을 하는 걸로 추측해볼 수 있겠다.
- Presentational 컴포넌트
데이터가 사용자에게 어떻게 보여지는가에 관심을 둔다.
ppt로 발표할 때 프레젠테이션이라는 단어를 쓰는데
이로부터 무언가 보여주는 역할을 한다고 생각해볼 수 있다.
예를 들어, 강아지 사진을 페칭하여 화면에 렌더하는 간단한 앱을 구현한다고 할 때,
import "./styles.css";
import DogImagesContainer from "./DogImagesContainer";
export default function App() {
return (
<div className="App">
<h1>
Dog Images
<span role="img" aria-label="emoji">
🐶
</span>
</h1>
<DogImagesContainer />
</div>
);
}
import { useEffect, useState } from "react";
import DogImages from "./DogImages";
// Container component
const DogImagesContainer = () => {
const [dogs, setDogs] = useState([]);
// 외부 api 로부터 데이터 페칭(부수효과)
useEffect(() => {
fetch("https://dog.ceo/api/breed/labrador/images/random/6")
.then((res) => res.json())
.then((data) => setDogs(data.message));
}, []);
// presentaional component에 props로 데이터를 전달
return <DogImages dogs={dogs} />;
};
export default DogImagesContainer;
import Loading from "./Loading";
// Presentational component
const DogImages = ({ dogs }) => {
// console.log(dogs);
if (dogs.length === 0) {
return <Loading />;
}
return (
<>{dogs && dogs.map((dog, i) => <img src={dog} key={i} alt={dog} />)}</>
);
};
export default DogImages;
Container/Presentational 패턴은 React Hooks로도 대체가 가능하다.
Container 컴포넌트 없이도 stateless 컴포넌트를 쉽게 만들 수 있게 되었다.
Container 컴포넌트의 데이터 페칭 로직을 커스텀 훅으로 만들고,
Presentational 컴포넌트에서 훅을 직접 호출해서 사용하는 것이다.
대신 이렇게 되면 상위 컴포넌트에서 바로 presentational 컴포넌트를 렌더해주고
그 안에서 훅으로 데이터를 불러와서 렌더릉 해주는 것이다.
import DogImages from "./DogImages";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>
Dog Images
<span role="img" aria-label="emoji">
🐶
</span>
</h1>
{/* Presentational 컴포넌트를 바로 렌더 */}
<DogImages />
</div>
);
}
Presentational 컴포넌트에서 활용할 데이터를 페칭해오고 상태를 가지는 커스텀 훅을 만든다.
이 훅은 페칭한 데이터를 반환하기 때문에 다른 컴포넌트에서 호출하여 이를 사용할 수 있다.
import { useEffect, useState } from "react";
const useDogImages = () => {
const [dogs, setDogs] = useState([]);
useEffect(() => {
async function fetchDogs() {
const res = await fetch(
"https://dog.ceo/api/breed/labrador/images/random/6"
);
const { message } = await res.json();
setDogs(message);
}
fetchDogs();
}, []);
return dogs; // 페칭한 데이터의 상태값를 반환함
};
export default useDogImages;
Presentational 컴포넌트에는 훅을 통해 불러온 데이터를 가지고 렌더링해준다.
import useDogImages from "./useDogImages";
const DogImages = () => {
const dogs = useDogImages();
if (dogs.length === 0) {
return <div>Loading...</div>;
}
return (
<>{dogs && dogs.map((dog, i) => <img src={dog} key={i} alt={dog} />)}</>
);
};
export default DogImages;
관심사를 분리할 수 있게 된다.
Presentational 컴포넌트는 UI를 담당하는 순수함수로 작성하게 되는 반면
Container 컴포넌트는 상태와 기타 데이터를 책임지게 된다.
Presentational 컴포넌트는 데이터 변경 없이 화면에 출력할 수 있으므로
재사용성이 높아진다.
또한 디자인 요구사항에 따라 수정이 필요한 경우 Presentational 컴포넌트를 수정하면
그것이 재사용하고 있는 부분 전체에 반영되기 때문에 유지보수성에 이점이 있다.
Presentational 컴포넌트는 테스트하기 쉽다. 일반적으로 부수효과가 없는 순수함수로 구현되므로 전체 Mock 데이터 스토어를 만들 필요 없이 요구하는 데이터만 인자로 넘겨주면 된다.
위에서 본 바와 같이 커스텀 훅을 활용하면 이 패턴을 사용하지 않아도 같은 효과를 얻을 수 있다.
이 패턴이든 훅을 사용한 방법이든 너무 작은 규모의 앱에서는 오버엔지니어링 일 수 있다.
Hooks는 Container/Presentational 컴포넌트를 완전히 대체하는 것인가?
다르다면 어떤 차이가 있을까?
이에 대해서는 이 블로그의 내용으로 생각을 더 해나가보면 좋을 것 같다.
https://yujonglee.com/socwithhooks