Reactjs의 개요
지난 몇 년간 JavaScript 를 사용하여 UI를 직접 구성하는것에 대한 수요가 증가했다. 이를 위해 UI를 개발하기 위해 페이스북이 개발한 오픈소스 라이브러리인 React가 사용되기 시작했다.
React 가 유일한 UI 라이브러리는 아니다. Preact, Vue, Angular, Svelte, Lit 과 같이 훌륭한 도구들이 있지만. React의 인기를 고려할 때 여기서 설명하려는 렌더링 패턴을 언급함에 있어 한번 살펴볼만한 가치가 있다.
개발자들은 웹 인터페이스를 위한 설계에 관련된 이야기를 많이 한다. 그리고 인터페이스의 주요 구성 요소는 버튼, 리스트, 네비게이션 등과 같은 것들이다. React는 이런 요소들을 조합해 인터페이스를 표현하기 위한 최적화되고 단순한 방법을 제시한다. 또 복잡하거나 구현하기 까다로운 인터페이스를 세 가지 주요 컨셉(Component, Props, State) 을 통해 쉽게 개발할 수 있도록 돕는다.
React는 컴포넌트에 초점이 맞춰져 있기 때문에 웹의 디자인 시스템의 요소들을 구성하는데 적합하다. 그래서 본질적으로 React기반으로 설계하게 되면 모듈식으로 구성하는것에 대해 시너지가 발생하며, 완성된 페이지나 뷰를 보기 전에 개별 컴포넌트를 디자인하는게 가능해지므로 각 요소의 범위와 목적에 대해 충분히 이해할 수 있다. 이 과정을 컴포넌트화 라고 한다.
사용하게 될 용어 정리
- React / React.js / ReactJS - 페이스북이 2013년에 개발한 라이브러리
- ReactDOM - DOM과 서버 렌더링을 위한 패키지
- JSX - JavaScript를 확장한 구문
- Redux - 중앙화된 상태 컨테이너
- Hooks - React에서 클래스 없이 상태 등을 사용할 수 있는 새로운 방법
- ReactNative - JavaScript를 이용하여 크로스 플랫폼 네이티브 앱을 개발할 수 있는 라이브러리
- Webpack - JavaScript 모듈 번들러, React 커뮤니티에서 유명함
- CRA (Create React App) - React 앱을 만들고 프로젝트를 생성할 수 있는 CLI도구
- Next.js - SSR, 코드 스플리팅, 성능 최적화, 기타 동급 최고의 기능을 포함한 React 프레임워크
JSX를 사용하여 렌더링하기
아래 예제에서는 JSX를 사용할 것이다. JSX는 JavaScript를 확장한 구문으로 XML과 유사한 형태의 HTML템플릿을 포함하고 있다. 이는 사용되기 전에 유효한 JavaScript구문으로 변경된다. 다만 변환 결과는 구현에 따라 달라질 수 있다. JSX는 React에서 많은 인기를 얻었지만 이후 다른 구현체도 만들어졌다.
Component, Props, State
실제로 React에서 보거나 수행할 모든 작업은 다음 주요 개념 중 적어도 하나로 분류할 수 있다. 아래에서 이러한 주요 개념을 간략히 살펴보자.
1. Component
컴포넌트는 React앱의 기본 빌딩 블록이다. 컴포넌트는 선택적으로 props를 받을 수 있고. 화면에 보여질 React엘리먼트들을 반환한다.
React앱에서 화면에 보여지는 모든 것들은 컴포넌트이다. React앱은 컴포넌트 안의 컴포넌트 안의 컴포넌트이다. 그래서 개발자는 페이지를 개발하는 것이 아니라 컴포넌트를 개발한다.
컴포넌트는 UI를 독립적이며 재사용 가능하도록 해 준다. 만약 페이지 위주의 디자인 경험만 있다면 이런 모듈 방식으로 디자인 하는것에 적응이 필요할 수 있다. 그러나 이미 디자인 시스템이나 스타일 가이드를 적용하고 있다면 아마 친숙할 것이다.
일반적으로 컴포넌트는 함수를 작성하여 만들 수 있다.
function Badge(props) {
return <h1>Hello, my name is {props.name}</h1>
}
위 함수는 프로퍼티가 담긴 props 객체를 인자로 받고 React엘리먼트를 반환하므로 유효한 React 컴포넌트이다. 이런 형태의 컴포넌트를 함수형 컴포넌트라 한다.
또 다른 형태의 컴포넌트로는 클래스 컴포넌트가 있다. 클래스 컴포넌트는 함수형 컴포넌트와 달리 ES6의 클래스 문법으로 정의할 수 있다.
class Badge extends React.Component {
render() {
return <h1>Hello, my name is {this.props.name}</h1>
}
}
컴포넌트 추출하기
컴포넌트를 여러 컴포넌트로 쪼갤 수 있다는 것을 설명하기 위해 아래 Tweet
예제를 준비했다.
위의 Tweet
컴포넌트는 아래와 같이 구현할 수 있다.
function Tweet(props) {
return (
<div className="Tweet">
<div className="User">
<img
className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="User-name">{props.author.name}</div>
</div>
<div className="Tweet-text">{props.text}</div>
<img
className="Tweet-image"
src={props.image.imageUrl}
alt={props.image.description}
/>
<div className="Tweet-date">{formatDate(props.date)}</div>
</div>
)
}
이 컴포넌트는 너무 큰 덩어리로 이루어져 있어 수정하기가 조금 어렵고 각 부분을 재사용하기 역시 어렵다. 그렇지만 여기서 몇 가지의 컴포넌트를 추출할 수 있다.
첫번째로 뽑아낼 것은 Avatar
컴포넌트이다.
function Avatar(props) {
return (
<img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} />
)
}
Avatar
컼포넌트는 Tweet
컴포넌트가 어떻게 렌더링 되는지에 대해서는 몰라도 된다. 따라서 Tweet
의 “작성자” 라는 prop 이름보다 “사용자” 라는 일반적인 prop이름을 사용했다.
이제 컴포넌트를 조금 단순화 할 수 있다.
function Tweet(props) {
return (
<div className="Tweet">
<div className="User">
<Avatar user={props.author} />
<div className="User-name">{props.author.name}</div>
</div>
<div className="Tweet-text">{props.text}</div>
<img
className="Tweet-image"
src={props.image.imageUrl}
alt={props.image.description}
/>
<div className="Tweet-date">{formatDate(props.date)}</div>
</div>
)
}
다음으로는 Avatar
컴포넌트 옆에 사용자명을 출력하는 User
컴포넌트를 만든다.
function User(props) {
return (
<div className="User">
<Avatar user={props.user} />
<div className="User-name">{props.user.name}</div>
</div>
)
}
이제 Tweet
컴포넌트를 조금 더 단순화할 수 있다.
function Tweet(props) {
return (
<div className="Tweet">
<User user={props.author} />
<div className="Tweet-text">{props.text}</div>
<img
className="Tweet-image"
src={props.image.imageUrl}
alt={props.image.description}
/>
<div className="Tweet-date">{formatDate(props.date)}</div>
</div>
)
}
컴포넌트를 추출하는 작업은 지루할 지 몰라도 이렇게 만들어낸 컴포넌트들은 큰 앱을 만들때 부담을 줄여준다. 컴포넌트를 위와 같이 단순화할 때의 좋은 기준은 다음과 같다: UI가 자주 재사용되거나 (버튼, 패널, 아바타) 캡슐화 할 만큼 복잡한 경우 (앱, 피드 스토리, 코멘트) 별개 컴포넌트로 분리하는것을 고려할 수 있다.
2. Props
Props는 컴포넌트의 프로퍼티이며, React의 컴포넌트들의 내부 데이터를 가리킨다. Props는 컴포넌트의 호출에서 만들어지고 다른 컴포넌트에게 전달된다. 또 prop="value"
처럼 HTML의 속성 구문과 동일하게 사용 가능하다. Props와 관련해서는 두가지를 기억하면 좋은데, 첫번째는 우린 컴포넌트 자체를 만들기 전에 prop으로 넘어갈 값들을 일종의 설계도 처럼 미리 정해놓을 것이다라는 것과, prop의 값은 변경할 수 없다는 것이다. 한번 컴포넌트에 들어오면 읽기만 가능하다는 것이다.
함수 컴포넌트는 prop
인자를 직접 사용하면 되고 클래스는 this.props
의 값을 찹조하는것으로 props를 사용할 수 있다.
3. State
State는 컴포넌트가 마운트되고 언마운트 되기 전에 변경될 수 있는 값들을 가지고 있는 객체이다. 컴포넌트의 Prop에 저장된 데이터의 현재 스냅샷과 같은 개념으로 볼 수 있다. State는 계속 변화하기 때문에 개발자가 원하는 시점에 원하는 화면을 그려내기 위해서는 변화하는 데이터를 관리할 방법이 필요한데. 이 것을 “상태 관리” 라고 한다.
상태 관리에 대한 이해 없이 React코드를 읽고 이해하기란 불가능에 가깝다. 개발자들은 이 주제에 대하여 설명하는 것을 좋아하지만. 근본적인 부분에서 상태 관리는 보기보다 그렇게 복잡하지는 않다.
React에서 State는 전역적으로 추적 가능하며 데이터는 필요에 따라 컴포넌트 간에 공유할 수 있다. 이것은 React 앱 내에서 데이터를 로드하는 동작이 다른 기술들과 비교해 리소스 차이가 크지 않다는 것을 의미한다. React앱은 데이터를 로드하고 저장하는 과정에서 스마트하게 동작하며 이는 데이터를 사용하는 인터페이스를 새로 그리는 계기가 된다.
React의 컴포넌트들을 각각의 데이터와 로직과 뷰를 가진 하나의 작은 앱들이라고 생각해 보자. 각 컴포넌트들은 각자의 목적이 있을것인데. 개발자로써 우리는 각 컴포넌트들이 어떤 데이터를 가지고 어떻게 동작할지를 제어한다고 생각하면 된다. 다른 컴포넌트의 데이터는 크게 고려하지 않아도 된다. 앱을 설계할 때 이 장점을 다양하게 활용할 수 있다. 이로써 UX를 개선하기 위해 추가적인 데이터를 더 보여줄 수도 있고 디자인 내 각 영역들을 보다 상황에 적합하게 개발할 수 있다.
React에 어떻게 상태를 추가해야 할까?
컴포넌트들을 설계할 때 상태를 추가하는것은 가급적 가장 마지막에 해야 한다. 최대한 Props와 이벤트만 가지고 개발하는것이 컴포넌트를 유지하기 쉽고, 테스트하기 쉽고, 이해하기 쉽게 만든다. 상태를 추가하는것은 Redux 혹은 Mobx 또는 컨테이너/래퍼 컴포넌트를 사용하는게 좋다. Redux는 반응형 프레임웍을 위한 유명한 상태 관리 시스템이며 액션을 통해 다룰 수 있는 중앙화된 상태 관리 기능을 구현할 수 있다.
아래 예제에서 LoginContainer
컴포넌트에 상태를 추가해야 한다. React의 Hooks를 이용하여 이를 구현해 보자.
const LoginContainer = () => {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const login = async (event) => {
event.preventDefault()
const response = await fetch('/api', {
method: 'POST',
body: JSON.stringify({
username,
password,
}),
})
// Here we could check response.status to login or show error
}
return (
<LoginForm onSubmit={login}>
<FormInput
name="username"
title="Username"
onChange={(event) => setUsername(event.currentTarget.value)}
value={username}
/>
<FormPasswordInput
name="password"
title="Password"
onChange={(event) => setPassword(event.currentTarget.value)}
value={password}
/>
<SubmitButton>Login</SubmitButton>
</LoginForm>
)
}
위 예제와 관련된 추가 예제는 Thinking in React 2020 에서 확인할 수 있다.
Props vs State
아래는 Props와 state의 주요 차이점이다.
Props | State |
---|---|
데이터는 컴포넌트 간 변경되지 않는 상태로 유지된다. | 데이터는 컴포넌트의 Props안에 저장된 현재 상태의 스냅샷이어서 컴포넌트의 생명 주기 동안 계속 변경된다. |
데이터는 읽기만 할 수 있다. | 🤔 데이터를 수정할 수 있다. |
props내의 데이터는 수정할 수 없다. | state내의 데이터는 this.setState를 통해 수정이 가능하다. |
props는 부모 컴포넌트에게 받는다. | state는 컴포넌트 내에서 관리된다. |
React의 기타 컨셉들
컴포넌트, Props, State는 React에서 다루게 될 주요 컨셉이다. 하지만 아래에서는 다른 컨셉들에 대해서도 학습해 보자.
1. 생명 주기
모든 컴포넌트는 마운트되고, 렌더링되며 언마운트되는 단계를 거친다. 이 사이에 발생하는 일련의 이벤트들을 컴포넌트의 생명주기라고 이야기한다. 이런 이벤트 자체는 부분적으로 컴포넌트의 내부의 상태와 관련이 있지만 생명주기는 조금 다르다. React는 필요에 따라 컴포넌트를 로드하거나 언로드하는 내부 코드가 있고. 컴포넌트는 각 단계에서 이 내부 코드를 사용한다.
생명 주기 메서드들 중에 가장 일반적인 것은:
render() - 이 메서드는 클래스 컴포넌트에서만 필요하며 React 내에서 가장 많이 구현된 메서드이다. 이름 그대로 마운팅되고 렌더링 되는 과정 중에 컴포넌트의 UI 렌더링을 처리한다.
컴포넌트가 생성되거나 제거될 때:
- componentDidMount() 컴포넌트의 출력 결과가 DOM에 렌더링되었을 때 실행된다.
- componentWillUnmount() 컴포넌트가 언마운트되고 제거될 때 즉시 호출된다.
Props혹은 State가 업데이트 될 때:
- shouldComponentUpdate() 는 새로운 prop이나 state를 받아 렌더링 되기 직전에 호출된다.
- componentDidUpdate()는 업데이트가 일어난 직후 호출된다. 이 메서드는 최초 렌더링 직후에는 호출되지 않는다,
2. 고차 함수 (HOC)
고차 함수(HOC) 는 React에서 컴포넌트 재사용을 위한 고급 기술이다. hoc는 기본적으로 컴포넌트를 받아 컴포넌트를 반환하는 함수를 뜻한다. 이 패턴은 React의 구성적 특징에서 나온 패턴이다. 컴포넌트가 props를 UI로 변환할 때 고차함수 컴포넌트는 특정 컴포넌트를 다른 컴포넌트로 변환하고 이는 주로 서드파티 라이브러리에서 우리가 구현한 컴포넌트에 특정 기능을 추가하기 위해 주로 사용한다.
3. 컨텍스트
보통 React에서 데이터는 props로 컴포넌트간에 전달되어 내려가는데. 이는 앱 내 여러 컴포넌트에서 필요로 하는데이터를 다루기에는 성가신 부분이 있다. 컨텍스트는 이런 종류의 데이터를 컴포넌트 계층 간에 명시적으로 넘기기 않더라도 사용할 수 있게 해 준다. 컨텍스트를 사용하면 특정 컴포넌트에게 데이터를 내려주기 위해 중간 컴포넌트에게 불필요한 데이터를 내려줄 필요가 없게 되는 것이다.
React Hooks
훅은 React의 상태나 생명주기 기능 중간에 개입할 수 있는 함수이다. 훅을 사용하면 클래스를 작성하지 않고도 state나 react의 기타 기능들을 활용할 수 있게 해 준다. 훅 패턴에서 관련된 내용을 학습할 수 있다.
React 로 앱을 구상해 보자
React로 앱을 구상하다 보면 생각보다 꽤 괜찮은 라이브러리임을 깨달을 수 있다. 이 섹션에서 상품 목록을 검색하는 앱을 만들면서 구상하는 과정을 경험해 보자.
첫번째 단계: Mock을 준비한다
아래 인터페이스를 위한 JSON API와 mock 이 있다고 가정해 보자.
JSON API는 아래와 같은 데이터를 응답한다.
;[
{
category: 'Entertainment',
retweets: '54',
isLocal: false,
text: 'Omg. A tweet.',
},
{
category: 'Entertainment',
retweets: '100',
isLocal: false,
text: 'Omg. Another.',
},
{
category: 'Technology',
retweets: '32',
isLocal: false,
text: 'New ECMAScript features!',
},
{
category: 'Technology',
retweets: '88',
isLocal: true,
text: 'Wow, learning React!',
},
]
위에서 사용한 도구는 Excalidraw이며 UI컴포넌트를 와이어프레이밍 해 볼때 매우 유용하다.
두번째 단계: UI를 컴포넌트 계층으로 나눈다
mock과 와이어프레임을 준비했다면 다음 할 일은 아래처럼 UI의 각 컴포넌트나 서브 컴포넌트에 박스를 그리고 각각 이름을 짓는 것이다.
이 때 “단일 책임 원칙을” 상기해 보자: 하나의 컴포넌트는 하나의 기능만을 담당한다. 컴포넌트가 커지면 서브컴포넌트로 나눠야 한다. 함수나 객체를 만들 때에도 같은 기준을 적용하자.
위 이미지에서 보는 것 처럼 앱을 구성하기 위해 다섯 컴포넌트를 만들어야 한다. 또 각 컴포넌트를 위한 데이터들도 준비해야 한다.
- TweetSearchResult (오랜지색): 전체 컴포넌트의 컨테이너 역할
- SearchBar (파란색): 검색을 위한 사용자 입력
- TweetList (초록색): 사용자의 검색에 따라 트윗을 보여주고 필터링한다
- TweetCategory (연청색): 각 카테고리의 제목을 나타낸다
- TweetRow (붉은색): 각 트윗의 행을 표현한다
와이어프레임으로부터 컴포넌트를 분리했으며 다음 할 일은 이를 계층 구조로 나타내 보는것이다. 화면에서 어떤 컴포넌트 내에 표현되는 컴포넌트를 아래와 같이 자식 컴포넌트로 나타낼 수 있다.
- TweetSearchResult
- SearchBar
- TweetList
- TweetCategory
- TweetRow
세번째 단계: React 컴포넌트를 구현한다
컴포넌트 계층을 그려본 뒤 할 일은 앱을 구현하는 것이다. 훅이 나오기 전 까진 빠르게 개발하기 위해서 데이터 모델과 UI만 구현하고 인터렉션을 생략했지만. 훅이 나온 후로는 아래와 같이 쉽게 개발할 수 있게 되었다.
i. 필터 가능한 트윗 목록
const TweetSearchResults = ({ tweets }) => {
const [filterText, setFilterText] = useState('')
const [inThisLocation, setInThisLocation] = useState(false)
return (
<div>
<SearchBar
filterText={filterText}
inThisLocation={inThisLocation}
setFilterText={setFilterText}
setInThisLocation={setInThisLocation}
/>
<TweetList
tweets={tweets}
filterText={filterText}
inThisLocation={inThisLocation}
/>
</div>
)
}
ii. SearchBar
const SearchBar = ({
filterText,
inThisLocation,
setFilterText,
setInThisLocation,
}) => (
<form>
<input
type="text"
placeholder="Search..."
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
<p>
<label>
<input
type="checkbox"
checked={inThisLocation}
onChange={(e) => setInThisLocation(e.target.checked)}
/>{' '}
Only show tweets in your current location
</label>
</p>
</form>
)
iii. 트윗 목록
const TweetList = ({ tweets, filterText, inThisLocation }) => {
const rows = []
let lastCategory = null
tweets.forEach((tweet) => {
if (tweet.text.toLowerCase().indexOf(filterText.toLowerCase()) === -1) {
return
}
if (inThisLocation && !tweet.isLocal) {
return
}
if (tweet.category !== lastCategory) {
rows.push(
<TweetCategory category={tweet.category} key={tweet.category} />
)
}
rows.push(<TweetRow tweet={tweet} key={tweet.text} />)
lastCategory = tweet.category
})
return (
<table>
<thead>
<tr>
<th>Tweet Text</th>
<th>Retweets</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
)
}
iv. 트윗 카테고리 행
const TweetCategory = ({ category }) => (
<tr>
<th colSpan="2">{category}</th>
</tr>
)
v. 트윗 행
const TweetRow = ({ tweet }) => {
const color = tweet.isLocal ? 'inherit' : 'red'
return (
<tr>
<td>
<span style="">{tweet.text}</span>
</td>
<td>{tweet.retweets}</td>
</tr>
)
}
최종적으로 구현된 결과는 직전에 설계해보았던 계층 구조와 같다.
- TweetSearchResults
- SearchBar
- TweetList
- TweetCategory
- TweetRow
React를 사용하는 방법
React를 사용하는 방법은 아래와 같이 다양한다
웹 페이지에 직접 로드하는 방법: react를 사용하는 가장 단순한 방법이다. npm 이나 CDN을 통해 JavaScript에 react를 추가한다.
create-react-app 사용하기: create-react-app은 가장 빨리 react 앱을 만드는데 초점을 두고 있는 프로젝트이며. SPA기능을 개발하다보면 필요한 서드파티 기능들이 필요한데 이것들도 쉽게 세팅해준다. 프로덕션 앱의 경우 코드 스플리팅과 같은 기능이 포함된 Next.js를 사용할 때 이런 도구들을 꼭 고려해보아야 한다.
Code Sandbox: 설치 없이 create-react-app 의 구조를 세팅할 수 있다. https://codesandbox.io 에 접속하여 “React” 를 선택하기만 하면 된다.
Codepen: React 컴포넌트를 프로토타이핑 할 때 유용하다. 다양한 [기본 구현체](https://codepen.io/flaviocopes/pen/VqeaxB들이 준비되어 있다.
결론
react.js는 모듈적이며 재사용 가능한 UI컴포넌트를 간단하고 직관적으로 만들 수 있도록 설계되어 있다. 이 가이드 문서를 읽으며 기본적인 개념을 어느정도 이해했기를 바란다.
react의 기초에 대해 조금 더 알아보려면 아래 링크를 참고 바란다.
이 가이드 문서는 react 컴포넌트와 prop에 대한 공식 문서, React로 생각하기, React 혹으로 생각하기, scriptverse의 문서가 없었다면 작성하기 어려웠을 것이다.