View
컴포넌트(Component)란 프로그래밍에 있어 재사용이 가능한 각각의 독립적인 모듈을 뜻한다. 프론트엔드에서 UI를 구성할 때 컴포넌트 기반으로 개발하면 마치 레고 블록을 쌓는 것처럼 이미 만들어진 컴포넌트들을 조합하여 화면을 구성할 수 있다.
컴포넌트들은 기능 구현 코드를 캡슐화하므로서 독립적으로 존재한다. 또한 재사용이 필요한 컴포넌트의 기능을 추상화하여 구현하므로서 재사용성에 매우 용이하다. 이러한 독립성과 재사용성으로 재사용을 원하는 어느곳이든 코드 충돌에 대한 걱정 없이 화면 구성이 가능하다는 큰 장점이 있다.
그 외에도 컴포넌트 기반 개발의 장점은 관심사 분리, 응집도 있는 로직 등이 있으며 독립성이 큰 장점 중 하나인 만큼 변경에 대해 엄청 유연해 유지보수 측면에서도 정말 좋다.
현대 대부분의 프론트엔드 개발은 React, Angular.js, Vue.js, Svelt.js 등의 프레임워크(또는 라이브러리)를 사용하여 컴포넌트 기반으로 개발하고 있다. 그 중 리액트는 자바스크립트의 라이브러리로 대표적인 컴포넌트 기반 개발의 유용한 도구이다. 리액트 문서에서 언급되어 있듯 컴포넌트의 개념은 리액트에서 가장 중요한 개념 중 하나이며 컴포넌트 기반 개발에 특화되어있다.
리액트 공식문서를 보면 ‘스스로 상태를 관리하는 캡슐화된 컴포넌트를 만드세요. 그리고 이를 조합해 복잡한 UI를 만들어보세요.’ 라는 문장이 있다. 더 쉽게 나와있는 문장은, ‘컴포넌트’라고 불리는 작고 고립된 코드의 파편을 이용하여 복잡한 UI를 구성하도록 돕습니다. 라고 리액트에서는 말한다.
리액트 공식문서에서 언급되어 있듯 컴포넌트를 최대한 작은 단위로 나누고 조합하여 작은 레고 블록으로 하나의 큰 성을 짓듯이 복잡한 UI를 구성하여 개발을 한다. 이처럼 컴포넌트는 하나의 기능을 담당하며 독립적이며 재사용성이 높아야 한다.
컴포넌트 정의
컴포넌트를 정의하는 방법은 클래스형 컴포넌트, 함수형 컴포넌트로 2가지로 나뉜다.
클래스형 컴포넌트 정의
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
함수형 컴포넌트 정의
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
최신 리액트에서는 함수형 컴포넌트로 정의하여 사용하기를 권장한다. 그럼에도 유지보수 측면에서 클래스형 컴포넌트에 대해 알아둘 필요가 있다.
| 클래스형 컴포넌트 | 함수형 컴포넌트 | |
| 장점 | - state와 LifeCycle API의 사용이 가능 -임의의 메서드를 정의할 수 있음 |
- 간편한 선언 - 클래스형 컴포넌트보다 적은 메모리 소비 - 파일 크기가 클래스형보다 작음 |
| 단점 | - 클래스 개념에 익숙하지 않은 프론트 개발자 - this 바인딩 이슈 - 로직들을 재사용하기 어려움 |
- state와 LifeCycle API의 사용이 가능 (16.8 Hook의 등장으로 해결 가능) |
컴포넌트 생명주기
리액트에서 모든 컴포넌트들은 같은 생명주기를 갖는다.
- 컴포넌트는 화면에 추가될 때 마운트된다. (생성)
- 컴포넌트는 새로운 속성 또는 상태를 받을 때 업데이트된다. (업데이트)
- 컴포넌트는 화면에서 사라질 때 언마운트된다. (삭제)
리액트 Hooks가 나오기 전 클래스형 컴포넌트에서 생명주기 메서드를 이용해 컴포넌트 생명주기에 따라 적절한 행위를 수행하였다. 많은 컴포넌트가 있는 애플리케이션에서 컴포넌트가 삭제될 때 해당 컴포넌트가 사용 중이던 리소스를 확보하는 것이 중요하다. 컴포넌트가 DOM에 렌더링이 될 때 마다 어떠한 동작을 하는 것을 리액트에서는 마운팅 이라 하며 해당 컴포넌트 DOM에서 삭제될 때마다 특정 기능이 수행되는 것을 리액트에서는 언마운팅 이라 한다.
이처럼 클래스형 컴포넌트에서는 특별한 메서드를 선언해 컴포넌트가 마운트되거나 언마운트 될 때 일부 코드를 동작시킬 수 있는데, 이러한 메서드들을 생명주기 메서드 라고 부르며 함수형 컴포넌트의 경우 useEffect 훅을 사용해 컴포넌트의 생명주기를 관리한다.
클래스형 컴포넌트 생명주기 메서드
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
// 마운팅
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
// 언마운팅
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
함수형 컴포넌트 생명주기
함수형 컴포넌트의 경우 클래스형 컴포넌트와는 차이가 있다. 우선 가장 큰 차이로는 Hook의 useEffect를 사용한다. 그리고 클래스형 컴포넌트의 경우 “컴포넌트”의 관점에서 생명 주기를 관리하였다면 함수형 컴포넌트의 경우 “Effect”에 중점을 두어 생명 주기를 관리한다.
리액트 문서에서는 이를 생명 주기라는 표현보다 “동기화(synchronization)”라는 표현을 자주 사용하며 “start/stop cycle”이라 표현하는 것 같다.
always focus on a single start/stop cycle at a time. It shouldn’t matter whether a component is mounting, updating, or unmounting. All you need to do is to describe how to start synchronization and how to stop it. If you do it well, your Effect will be resilient to being started and stopped as many times as it’s needed.
useEffect를 다루는 내용이 아니니 간단하게만 작성해보았다.
useEffect(() => {
// 코드 적는 곳
return () => {
// return 함수를 `cleanup function`이라 하며 Effect가 어떻게 동기화를 멈추는지에 대해 작성하면된다.
// unmount나 unsubscribe 코드
}
}, [dependency])
Deep Dive
앞서 도입부에서 ‘컴포넌트’라고 불리는 작고 고립된 코드의 파편을 이용하여 복잡한 UI를 구성하도록 돕습니다. 라고 리액트 문서에서 말한다고 했다. 그렇다면 컴포넌트를 최대한 작은 단위로 나눈다는 것은 무슨 의미일까? 얼마나 작게 쪼개어야 한다는 것일까? 하나의 버튼이 “확인”, “취소”, “회원 가입”, “로그인” 등 다양한 역할을 수행하는 하나의 범용적인 버튼으로 만들어야 한다는 것일까?
보통 객체 지향 설계의 5원칙 SOLID의 SRP(단일 책임의 원칙)에 따라 컴포너트는 “하나의 컴포넌트가 하나의 책임만을 담당하는 것이 좋다.”라고 말한다. 여기서 책임이란 정확히 무엇일까? 카카오 기술 블로그에서 작성된 이 글에서는 “컴포넌트를 적절하게 나눈다는 것은 컴포넌트의 책임을 적절하게 나눈 것”이라고 한다.
또한 “어떻게 책임을 나누는 것이 좋을까?”에 대한 고민의 대답도 있다.
컴포넌트의 책임을 어떻게 나눌 것인가에 대해서 현실적으로는 훨씬 세분화해서 나눌 수 있지만 간단한 이해를 위해 설명하면 애플리케이션의 데이터를 중심으로 컴포넌트는 크게 두 가지로 나눌 수 있다.
- 어떤 도메인과도 상관없는 범용적인 컴포넌트
- 특정 도메인에 종속된 비즈니스 컴포넌트
범용적인 컴포넌트
- 도메인에 종속되지 말자
- 추상화할 때는 특정 도메인과의 결합도를 낮추고 재사용성을 높이는 것이 중요하며, 너무나 많은 역할을 수행하지 않도록 주의해야한다.
- 특정 도메인과의 높은 의존성은 변경에 굉장히 취약하다.
- 특정 도메인이 아닌 다른 컴포넌트들이 사용함에 있어 필요한 공통의 기능을 수행한다는 책임을 중요하게 생각해야 한다.
- 의존성을 없애야 다른 컴포넌트와의 결합도도 매우 낮아진다.
- 합성을 활용하자
- 도메인 의존성과는 별개로 컴포넌트 자체가 수행하는 책임이 너무 커도 문제가 된다.
- 합성을 활용해 하나의 컴포넌트가 가진 책임을 잘게 나누어 유연한 대응을 할 수 있도록 한다.
- 합성은 필요하다면 어디서나 유연한 구조로 컴포넌트를 설계하기 좋은 패턴이다.
- 무분별하게 책임을 잘게 나누어 불필요하게 합성하는 구조는 지양하자.
- 가능하다면 prop만으로 컴포넌트를 만드는 것이 훨씬 단순하고 유지보수나 가독성 측면에 좋다.
도메인과 컴포넌트
도메인과 어쩔 수 없이 의존성을 가지는 컴포넌트도 존재한다. 이러한 컴포넌트들은 특정한 비즈니스 로직(서버와 통신, 데이터 가공 등)을 실행하기 위해서 꼭 필요하다. 이때 너무 많은 컴포넌트가 도메인과 얽히지 않도록 설계하는 것이 중요하다.
그럼 도메인에 너무 많은 컴포넌트가 얽히지 않게 설계하려면 어떻게 해야 할까?
- 커스텀 훅을 이용한다.
- 리액트 훅은 비즈니스 로직들을 추상화하기 아주 좋은 API다.
- 로직들을 훅으로 분리함으로써 컴포넌트에는 렌더링을 위한 코드만 응집되고 가독성이 좋아진다.
- 별도의 훅으로 분리하였기 때문에 각각을 단위 테스트로 검증하기 좋습니다.
- 과거 React에서 비즈니스 로직을 분리하기 위해 사용했던 Render Props나 HOC에 비해 훅은 훨씬 단순하게 비즈니스 로직을 컴포넌트에 합성할 수 있다.
- 그리고 훅을 사용한 책임의 추상화는 여러 컴포넌트가 도메인과 무분별하게 얽혀 망가지는 것을 방지해줍니다.
- 상태 관리와 데이터
컴포넌트에 대한 포스팅은 여기서 마무리하겠습니다. `Deep Dive` 부분의 카카오 기술 블로그의 글에서 컴포넌트를 잘게 나누는 것에 대한 예시 코드와 함께 디테일하고 친절하게 설명되어 있어 한 번쯤 읽어보시면 좋을 것 같습니다🙂
[참고사이트]
리액트 공식문서
https://react.dev/learn/lifecycle-of-reactive-effects#thinking-from-the-effects-perspective
https://hanamon.kr/컴포넌트-component란/
https://fe-developers.kakaoent.com/2022/221020-component-abstraction/
'Language > ReactJS' 카테고리의 다른 글
| [React] Context API 톺아보기 (1) | 2023.09.02 |
|---|---|
| [React] 상태 관리 라이브러리의 필요성 - Redux (1) | 2023.04.24 |
| [React] Context API - useContext (2) | 2023.04.17 |
| [React] JSX 이해하기 (0) | 2023.04.03 |
| [React] 안녕 CRA, 안녕 Vite (0) | 2023.03.28 |
