이제껏 component를 제작할 때, 함수형 component만을 사용했었다.
React에서는 함수형이 아닌 다른 방법으로 component를 빌드할 수 있다.
React hook에서도 잠깐 다뤘지만, Class를 이용해서 component를 제작할 수 있다.
물론, hook이 도입된 이후로 React project에서는 거의 이 방법을 사용하지 않는다.
따라서, 이부분은 생략을 해도 무관하지만 아직 여전히 많은 third party library나 이미 제작된 project에서 볼 가능성이 있다.
Functional Components
그 전에 이제까지 배웠던 함수형 component에 대해서 정리하고 넘어가자.
function Product(props) {
return <h2>A Product!</h2> ;
}
// or
const Product = (props) => {
return <h2>A Product!</h2> ;
} ;
function
keyword 대신 const
와 arrow function을 사용해서 구현할 수도 있는데 어느 쪽이든, 단순히 함수를 만드는 것이다.
또한, props를 받는 함수를 만들며 화면에 렌더링할 JSX 코드를 return시켰다.
이러한 함수들은 JSX와 함께 렌더링 가능한 결과들이 담긴 JavaScript 함수다.
굳이 "함수형" component라고 언급하는 이유는 이것이 component를 만드는 유일한 방법이 아니기 때문이다.
component를 정의하는 다른 방법이 있으므로 함수형 component라고 부르는 것이다.
🎈Components are regular JavaScript functions which return renderable results (typically JSX)
🎈Most Modern Approach
Class-based Components
다른 대안으로 JavaScript의 기본 기능 중 하나인 class를 생성하는 것으로 component를 제작할 수 있다.
class는 React의 기능이 아닌 modern JavaScript에 존재하는 기능이다.
class Product extends Component{
render(){
return <h2>A Product!</h2> ;
}
}
그리고 render()
방식으로 정의할 수 있다. 여기서 render
는 예약어로 반드시 해당 단어를 사용해야 한다.
React가 render()
method를 호출해서 무엇이 스크린에 렌더링되어야 하는지를 평가한다.
이를 class component 또는 class-based component라고 부른다.
🎈Components can also be defined as JS classes where a render() method defines the to-be-rendered output
🎈최근에는 Error Boundaries의 예외 때문에라도 class component를 사용할 이유가 없다.
Why Class Components Exist?
class component가 존재하는 이유는 과거, React 16.8.0 버전[2019년도 2월 6일] 이전에 사용되었기 때문이다.
특히, state나 side effect를 처리하기 위해서는 class component를 사용했어야 했다.
React 16.8.0 버전 이전에는 함수형 component에서의 state 변경이 불가능했고 side effect 역시 다룰 수 없었다.
오직 class component에서만 가능했었다. 때문에 더 높은 상호작용이 가능한 사용자 interface를 지원하기 위해서는 필수적이었다.
React 16.8.0 버전 이후에는 React에서 Hook이라는 개념을 도입하였고, 앞서 배운 useState()
와 같은 hook을 사용할 수 있게 되었다.
How to convert from functional component to class-based component?
아래 예제 코드는 함수형 component이다.
import classes from './User.module.css';
const User = (props) => {
return <li className={classes.user}>{props.name}</li>;
};
export default User;
이처럼 간단한 component의 경우 다음과 같이 re-building할 수 있다.
import { Component } from 'react' ;
import classes from "./User.module.css";
class User extends Component{
constructor() {}
render() {
return <li className={classes.user}>{this.props.name}</li>;
}
}
export default User;
class 예약어를 입력하여 JavaScript에 build한다. 추가적으로 constructor에 값을 할당해줄 수 있지만 여기서는 사용하지 않는다.
render()
method를 사용하여 React에게 무엇을 화면에 렌더링해야하는지 알려준다.
따라서, 함수형 component의 return
값과 동일하게 사용가능하다. 조금 다른 점이 있다면 props를 받는 방법이다.
class의 경우에는 extend Component를 하여 props 객체를 받는다.
extend는 다른 class로부터 상속을 받는 것으로 React.Component
class에게 상속받은 값들을 사용할 수 있게 된다.
결론적으로, 이 둘은 모두 component로 필요하다면 같이 섞어서 사용해도 무관하다.
실제 여러 project를 진행할 때, 대부분의 경우 함수형 component를 사용하겠지만 이처럼 class component도 사용할 수 있다.
State & event handler
이어서 state를 가진 component를 re-building해보자
import { useState } from "react";
import User from "./User";
import classes from "./Users.module.css";
const DUMMY_USERS = [
{ id: "u1", name: "Max" },
{ id: "u2", name: "Manuel" },
{ id: "u3", name: "Julie" },
];
const Users = () => {
const [showUsers, setShowUsers] = useState(true);
const toggleUsersHandler = () => {
setShowUsers((curState) => !curState);
};
const usersList = (
<ul>
{DUMMY_USERS.map((user) => (
<User key={user.id} name={user.name} />
))}
</ul>
);
return (
<div className={classes.users}>
<button onClick={toggleUsersHandler}>
{showUsers ? "Hide" : "Show"} Users
</button>
{showUsers && usersList}
</div>
);
};
export default Users;
state를 사용하기 위해서는 항상 2가지 작업을 해줘야한다. 초기화와 state 정의
초기화는 useState()
를 처음 호출할 때, parameter로 넣어주면서 사용했었다.
state 정의는 toggleUsersHandler()
와 같은 event handler로 업데이트가 필요한 state를 정의하는 것이다.
class component에서는 state를 생성할 때는 생성자(constructor)를 사용한다.
이 구문이 instance할 때, 즉 React가 component로 사용되는 class를 만날 때 자동적으로 호출된다.
constructor() {
// super(): class가 extends한다면, 상위 class의 constructor를 호출하는 method를 사용해야함.
super() ;
this.state = { showUsers: true, test: "demo" };
}
위와 같이 this.state
에 접근해서 객체로 설정해줄 수 있다. class component에서는 항상 state는 객체 형태이다.
함수형 component에서는 boolean, string, number, object 등 어떠한 형태로든 가능할 정도로 유연했지만 class component는 아니다.
class component에서는 여러 state들을 하나의 객체로 관리하기 때문에 항상 객체로 사용된다.
함수형 component에서는 useState()
를 여러번 호출하기도, 객체로 그룹화할지도 선택사항이었는데 class component는 강제성을 띈다.
그리고 toggleUsersHandler()
와 같이 state를 변경하고자 할 때에는 this.setState()
라는 특수한 method를 사용해야한다.
toggleUsersHandler() {
this.setState({ showUsers: false });
}
state를 초기화 했을 때와 마찬가지로, 새로운 객체 형태로 넣어주면 된다. 함수형 component에서는 항상 덮어씌우는 방식이었다.
하지만 기존의 상태를 override하지 않고 React가 기존에 존재하는 state에 여기서 전달하는 객체를 결합시킨다.
당연히 같은 속성의 값은 override되겠지만 기존에 지닌 다른 state의 값과 병합하게 된다.
함수형 component에서는 병합해주는 로직을 스스로 구현해야했다.
하지만 onClick
과 같은 event를 통해 method가 호출되어도 기본적으로 동작하지 않는다.
<button onClick={this.toggleUsersHandler.bind(this)}/>
때문에 위와 같이 bind를 사용하여 this 예약어가 코드가 평가될 시점의 동일한 값이나 내용을 갖도록 설정하는 것이다.
그렇게 해서 re-building한 코드는 다음과 같다.
import { Component } from "react";
import User from "./User";
import classes from "./Users.module.css";
const DUMMY_USERS = [
{ id: "u1", name: "Max" },
{ id: "u2", name: "Manuel" },
{ id: "u3", name: "Julie" },
];
class Users extends Component {
constructor() {
super() ;
this.state = { showUsers: true, test: "demo" };
}
toggleUsersHandler() {
this.setState((currState) => {
return { showUsers: !currState.showUsers };
});
}
render() {
const usersList = (
<ul>
{DUMMY_USERS.map((user) => (
<User key={user.id} name={user.name} />
))}
</ul>
);
return (
<div className={classes.users}>
<button onClick={this.toggleUsersHandler.bind(this)}>
{this.state.showUsers ? "Hide" : "Show"} Users
</button>
{this.state.showUsers && usersList}
</div>
);
}
}
export default Users;
Side Effect - Lifecycle
앞에서 class component로 state를 관리하는 방법에 대해서 알아봤다. 그럼 side effect는 어떤가?
class component는 React hook을 사용할 수 없기 때문에 useEffect()
를 사용할 수 없다.
하지만 class component에는 lifecycle이라는 개념이 존재한다. (사실 모든 component에 존재한다.)
많은 component가 있는 애플리케이션에서는 component가 삭제될 때 해당 component가 사용 중이던 리소스를 확보하는 것이 중요하다.
component가 처음 DOM에 렌더링 될 때를 React에서 마운트(mount)이라고 한다.
또한 component에 의해 생성된 DOM이 삭제될 때를 React에서 언마운트(unmount)라고 한다.
component class에서 특별한 method를 선언하여 component가 마운트되거나 언마운트 될 때 일부 코드를 작동할 수 있다.
이러한 method들을 lifecycle이라고 부르며, 다른 2개의 class component를 서로 다른 시점에서 추가할 수 있게 한다.
먼저 중요한 것은 class component를 추가할 수 있는 lifecycle method는 componentDidMount()
method이다.
render()
method와 같은 내장 함수로 React에서 불러오면(import
) 바로 사용 가능하다.
이 method를 추가한다면 React가 component를 마운트한 직후에 이 method를 호출할 것이다.
또 다른 lifecycle method는 componentDidUpdate()
와 componentWillUnmount()
가 있다.
간단히 정리하자면 다음과 같이 해석할 수 있을 것이다.
In class-based component | In functional component | 동작 |
componentDidMount() |
useEffect(() => {}, []) |
component가 처음 mount될 때 |
componentDidUpdate() |
useEffect(() => {}, [someValue]) |
component의 someValue가 갱신되었을 때 |
componentWillUnmount() |
useEffect(() => { return () => {cleanUp}}, []) |
component가 unmount되기 직전일 때 |
아래는 UserFinder component를 class로 변환한 코드다
import { Fragment, useState, useEffect } from "react";
import Users from "./Users";
import classes from "./UserFinder.module.css";
const DUMMY_USERS = [
{ id: "u1", name: "Max" },
{ id: "u2", name: "Manuel" },
{ id: "u3", name: "Julie" },
];
const UserFinder = () => {
const [filteredUsers, setFilteredUsers] = useState(DUMMY_USERS);
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
setFilteredUsers(
DUMMY_USERS.filter((user) => user.name.includes(searchTerm))
);
}, [searchTerm]);
const searchChangeHandler = (event) => {
setSearchTerm(event.target.value);
};
return (
<Fragment>
<div className={classes.finder}>
<input type="search" onChange={searchChangeHandler} />
</div>
<Users users={filteredUsers} />
</Fragment>
);
};
export default UserFinder;
import { Fragment, Component } from "react";
import Users from "./Users";
import classes from "./UserFinder.module.css";
const DUMMY_USERS = [
{ id: "u1", name: "Max" },
{ id: "u2", name: "Manuel" },
{ id: "u3", name: "Julie" },
];
class UserFinder extends Component {
constructor() {
super();
this.state = {
filteredUsers: [],
searchTerm: "",
};
}
componentDidMount(){
// 📶 Send http request...
this.setState({filteredUsers: DUMMY_USERS}) ;
}
componentDidUpdate(prevProps, prevState) {
if (prevState.searchTerm !== this.state.searchTerm) {
this.setState({
filteredUsers: DUMMY_USERS.filter((user) =>
user.name.includes(this.state.searchTerm)
),
});
}
}
searchChangeHandler(event) {
this.setState({ searchTerm: event.target.value });
}
render() {
return (
<Fragment>
<div className={classes.finder}>
<input type="search" onChange={this.searchChangeHandler.bind(this)} />
</div>
<Users users={this.state.filteredUsers} />
</Fragment>
);
}
}
export default UserFinder;
이로써 class component에서도 side effect를 처리할 수 있게 되었다.
물론 그 과정이 useEffect 보다 복잡하고 코드가 길어지게 된다.
잘 쓰이지는 않겠지만 알아두면 좋을 것 같다.
Context API
마찬가지로 class component에서의 context는 어떻게 적용할까?
만약 함수형 component였다면 useContext()
를 사용하면 쉽게 해결할 수 있다.
하지만 class component이기 때문에 불가능하다. 이에 대한 대안으로 2가지가 있다.
첫 번째는 Context.consumer
component를 사용하는 것이다. 이는 JSX에서만 사용 가능하기 때문에 함수형 class형 모두 사용가능하다.
이외에 다른 방법으로는 static
property를 추가하는 것이다. static
이라는 예약어를 사용하고 contextType
이라는 property를 만들 수 있다.
하지만 이 static contextType
은 한 component에서 단 한번만 설정할 수 있다는 제약조건이 있다.
때문에 동시에 연결해야 하는 다수의 context인 경우 다른 방법을 사용해야한다.
대신 1개의 context를 이용하는 경우에는 this.context.{객체 이름}
으로 바로 접근이 가능하다는 장점이 있다.
사용법이 굉장히 간단하지만, 유연성이 조금 떨어진다.
아래 코드는 예제코드로 user의 정보를 context로 받아오는 코드를 class component로 변환한 것이다.
import { Fragment, useState, useEffect, useContext } from "react";
import Users from "./Users";
import classes from "./UserFinder.module.css";
import UsersContext from "../store/users-context";
const UserFinder = () => {
const userCxt = useContext(UsersContext) ;
const [filteredUsers, setFilteredUsers] = useState(userCxt.users);
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
setFilteredUsers(
userCxt.filter((user) => user.name.includes(searchTerm))
);
}, [searchTerm]);
const searchChangeHandler = (event) => {
setSearchTerm(event.target.value);
};
return (
<Fragment>
<div className={classes.finder}>
<input type="search" onChange={searchChangeHandler} />
</div>
<Users users={filteredUsers} />
</Fragment>
);
};
export default UserFinder;
import { Fragment, Component } from "react";
import Users from "./Users";
import classes from "./UserFinder.module.css";
import UsersContext from "../store/users-context";
class UserFinder extends Component {
static contextType = UsersContext;
constructor() {
super();
this.state = {
filteredUsers: [],
searchTerm: "",
};
}
componentDidMount() {
this.setState({ filteredUsers: this.context.users });
}
componentDidUpdate(prevProps, prevState) {
if (prevState.searchTerm !== this.state.searchTerm) {
this.setState({
filteredUsers: this.context.users.filter((user) =>
user.name.includes(this.state.searchTerm)
),
});
}
}
searchChangeHandler(event) {
this.setState({ searchTerm: event.target.value });
}
render() {
return (
<Fragment>
<div className={classes.finder}>
<input type="search" onChange={this.searchChangeHandler.bind(this)} />
</div>
<Users users={this.state.filteredUsers} />
</Fragment>
);
}
}
export default UserFinder;
Error Boundary
어플리케이션에서 가끔 문제를 발생시키곤 하는 것이 있다. 개발자의 관점에서 버그를 말하는 것이 아니라 예방할 수 없는 오류이다.
어떤 오류는 어플리케이션의 어떤 부분에서 다른 부분으로 무언가 잘못되었다는 것을 전달하기도 한다.
예를 들어, HTTP request를 보자.
서버가 일시적으로 응답이 없을 경우에는 이 request에 대한 응답을 할 수 없으며, 어플리케이션 측에서는 오류가 발생된 것으로 보게 된다.
🧑💻 이런 경우 개발자가 무엇을 할 수 있을까? 일반적인 JavaScript에서는 try-catch
문을 사용한다.
try
를 통해 실패할 수 있는 코드를 쓰고 잠재적으로 발생할 수 있는 오류를 감지한다.- 이후, 감지된 오류를
catch
에서 오류 처리를 위한 대체 코드를 실행하는 방법이다.
React에서도 당연히 JavaScript를 사용하지만 자식 component 안에서 오류가 발생하고 부모 component에서 처리할 수 없다.
이 문법은 정규 JavaScript 문장을 사용하는 곳에서만 쓸 수 있기 때문에 try-catch
문을 사용할 수 없다.
React에서 component 안에서 발생한 오류는 JSX 코드 안에서 발생한 것이기 때문에 try-catch
로 오류처리가 불가능하다.
대신에 이럴 때 오류 경계(Error Boundary)라는 것을 만들어서 처리할 수 있다.
바로, componentDidCatch()
method를 class component에 추가하여 만들게 된다.
여기서 Error Boundary라는 단어는 이러한 lifecycle
method를 갖는 component를 자칭하는 용어다.
2개의 함수형 component는 편집할 수 없으며, 현재 이 Error Boundary에 해당하는 함수형 component는 존재하지 않는다.
따라서, Error Boundary를 build하기 위해서는 반드시 class component이면서 lifecycle
을 갖는 component여야 한다.
componentDidCatch()
method는 하위 component 중 하나가 오류를 만들거나 전달할 때 발동된다.
Error Boundary에서도 마찬가지로 render()
를 호출하는 데 특이한 점은 this.props.children
을 return
한다는 것이다.
여기서 this.props.children
을 return
해주는 이유는 이 Error Boundary로 보호하고자하는 component를 둘러싸야하기 때문이다.
Error Boundary 안에서도 오류가 발생했을 때의 조치 사항을 작성해줘야한다.
이를 위해 componentDidCatch()
는 error 객체를 parameter로 가져온다. 이는 React가 자동으로 전달해준다.
그리고 오류를 확인한 후 무슨 문제인지 개별 오류마다 서로 다른 로직을 실행할 수 있다.
물론 각 오류마다 개별적인 Error Boundary가 필요하지만, 오류를 분석하기 위해 서버로 전송한다던가 하는 작업도 가능하긴 하다.
여기 임시로 오류를 만들었다. user의 수가 0이 되면 Error를 실행하는 코드이다.
import { Component } from "react";
import User from "./User";
import classes from "./Users.module.css";
class Users extends Component {
constructor() {
super() ;
this.state = { showUsers: true, test: "demo" };
}
✨
componentDidUpdate() {
if(this.props.users.length === 0){
throw new Error('No users provided!') ;
}
}
✨
toggleUsersHandler() {
this.setState((currState) => {
return { showUsers: !currState.showUsers };
});
}
render() {
const usersList = (
<ul>
{this.props.users.map((user) => (
<User key={user.id} name={user.name} />
))}
</ul>
);
return (
<div className={classes.users}>
<button onClick={this.toggleUsersHandler.bind(this)}>
{this.state.showUsers ? "Hide" : "Show"} Users
</button>
{this.state.showUsers && usersList}
</div>
);
}
}
export default Users;
이에 반응하기 위해 ErrorBoundary를 제작했다. Error가 detect되면 다른 요소를 return 해준다.
import React, { Component } from "react";
class ErrorBoundary extends Component {
constructor() {
super();
this.state = { hasError: false };
}
componentDidCatch(error) {
this.setState({ hasError: true });
}
render() {
if (this.state.hasError) {
return <p>Something went wrong!</p>;
}
return this.props.children;
}
}
export default ErrorBoundary;
아래는 Error Boundary를 사용하기 위해 감싸줬다(Wrap).
import { Fragment, Component } from "react";
import Users from "./Users";
import ErrorBoundary from './ErrorBoundary';
import classes from "./UserFinder.module.css";
import UsersContext from "../store/users-context";
class UserFinder extends Component {
static contextType = UsersContext;
constructor() {
super();
this.state = {
filteredUsers: [],
searchTerm: "",
};
}
componentDidMount() {
this.setState({ filteredUsers: this.context.users });
}
componentDidUpdate(prevProps, prevState) {
if (prevState.searchTerm !== this.state.searchTerm) {
this.setState({
filteredUsers: this.context.users.filter((user) =>
user.name.includes(this.state.searchTerm)
),
});
}
}
searchChangeHandler(event) {
this.setState({ searchTerm: event.target.value });
}
render() {
return (
<Fragment>
<div className={classes.finder}>
<input type="search" onChange={this.searchChangeHandler.bind(this)} />
</div>
✨
<ErrorBoundary>
<Users users={this.state.filteredUsers} />
</ErrorBoundary>
✨
</Fragment>
);
}
}
export default UserFinder;
이 Error Boundary를 추가하기 위해서는 class component를 사용해야만 한다.
아직 함수형 component로 사용 불가능하다.