본문 바로가기

FrontEnd+

[React] 공식문서로 시작하는 리액트 입문 1편

 

우아한테크코스 프론트엔드 과정의 레벨 2는 리액트로 진행된다고 한다. 레벨2를 앞두고 아주 기초적인 개념이라도 훑어보려고 한다. 리액트는 공식 홈페이지에서 제공하는 공식문서가 잘 정리되어있다고 해서 가볍게 문서만 읽고 정리해보자.
( + 리액트의 공식문서는 풍부한 사전지식을 전제로 한 설명이 섞여 있어 친절한 편은 아니라고 한다...! )
공식 홈페이지를 훑어보니 크게 주요 개념을 단계적으로 설명해주는 '기본 가이드'와 실습 위주로 간단한 게임을 따라 만드는 '실전 튜토리얼'로 구성되어 있었다. 이 글에서는 '기본 가이드'의 개념 위주로 정리하겠다.

 

 

 

01. 왜 리액트일까?

페이스북에서 리액트를 만든 이유

2013년 5월, 페이스북에서 '리액트'를 발표했다. 그리고 같은 해 7월 Facebook Developers 유튜브 채널에는 "Introduction to React.js" 영상이 올라왔다. 영상에서는 페이스북 리액트 창시자(?) Jordan Walke와 Tom Occhino가 페이스북에서 왜 리액트를 만들게 되었는지 설명하고 있다. 리액트는 '웹 브라우저에서 구동하는 JS 어플리케이션을 어떻게 구조화시킬 것인가'🤔 라는 고민의 결과물이라고 한다.
Tom Occhino가 얘기한 것과 같이 우리는 다음과 같은 방식에 익숙하다. 초기 렌더를 마치고 나서 데이터(Model)의 변화가 생기면, 이를 관찰하고 있던 View가 해당 내용을 업데이트(mutate)한다. 하지만 각각의 데이터가 변할 때마다 View를 업데이트시켜줄 수 있도록 일일이 코드를 작성하는 것은 상당히 복잡한 일이다.
페이스북 채팅 수정기능(rewrite)을 구현할 당시, Tom의 팀에서는 데이터 변경에 따라 View의 어떤 부분을 변경할지 개발자가 일일이 신경 써야 하는 것을 최소화하고 싶었다. 이 복잡한 작업을 피할 수 있는 아주 쉬운 방법이 있다. 데이터를 반영해야 하는데 어떻게 그럴 수가 있나 싶겠지만 아무튼 방법이 있다! 바로, 데이터가 변경될 때마다 기존의 View를 수정하는 것이 아니라 아예 새로 View를 그리는 방법이다.

데이터가 변경될 때마다 기존의 View를 날리고 새로운 View를 그린다.

가상돔(Virtual DOM)

그런데 기존의 View를 몽땅 떤져버리고 새로 그린다면 어마어마한 DOM조작으로 인해 성능이 떨어지게 될 것이다. 이 문제를 해결하기 위해 리액트는 가상돔을 두고 이전의 가상돔과 현재 가상돔을 비교해서 스마트하게 바뀐 부분의 View를 새로 넣어준다. "React and the Virtual DOM" 영상(3분+)은 가상돔의 컨셉을 애니메이션으로 쉽게 보여준다.

 

02. JSX 입문

JSX를 왜 써야할까?

JSX는 JavaScript 확장 문법이라고 한다. 그런데 생긴 것은 HTML과 많이 닮아있다. 왜 JS의 확장 문법을 HTML처럼 사용할까? 리액트 공식문서에서는 JS와 HTML이 결합한 듯한 이 문법에 대해 의아해하는 독자를 위해, 2013년 JSConf EU 영상을 참고할 것을 추천한다.
영상의 발표자는 Pete Hunt는 디스플레이 로직과 마크업은 필연적으로 서로 결합될 수밖에 없는 사실을 지적한다. 그래서 리액트에서는 디스플레이 로직과 마크업을 인위적으로 분리(별도에 파일로 분리)하는 대신에, 디스플레이 로직과 마크업이 느슨하게 연결된 유닛, '컴포넌트(Component)'를 도입한 것이다.

React build components, not templates.
React component is a highly cohesive building block for UIs loosely coupled with other components.
JSX is an 'optional' preprocessor to let you use HTML-like syntax.

리액트에서 반드시 JSX를 사용해야 하는 것은 아니다. 하지만 JSX를 사용하면 중괄호 안에서 자바스크립트의 모든 문법을 사용할 수 있고, HTML과 비슷한 스타일의 구문으로 작성할 수 있기 때문에 대부분의 경우에 사용된다.

// JSX 적용 (X) 

const message = React.Dom.div(
    { className: 'hello', onClick: someFunc }, 
    [ React.DOM.span(null, ['Hello World']) ]
);
// JSX 적용 (O) 

const message = ( 
  <div className="hello" onClick={someFunc}> 
    <span>Hello World</span> 
  </div> 
);

 

JSX 기초문법

JSX는 하나의 표현식이라고 할 수 있다. 따라서 가독성을 위해 여러 줄로 나누어 적더라도, 세미콜론이 자동으로 삽입되는 것을 방지하기 위해 위와 같이 괄호로 묶어주는 것을 권장한다. JSX attribute를 작성할 때는 className="hello"처럼 따옴표를 이용해 문자열을 넣거나, onClick={someFunc} 처럼 중괄호를 이용해 JS 표현식을 삽입할 수 있다. React DOM은 렌더하기 전에 JSX에 들어가는 모든 값을 문자열로 변환하기 때문에, XSS 공격(Cross-Site-Scripting)으로 부터 안전하다.
JSX는 HTML보다는 JavaScript에 가깝기 때문에, 리액트에서는 HTML의 attribute 가 아닌 property의 네이밍 컨벤션(카멜케이스)를 따른다. class는 className으로, font-size는 fontSize로 작성해야 한다는 말이다. 한편, <img>, <input>, <br>과 같은 '셀프클로징 태그(void elements)'를 <img />, <input />, <br />로 적고 태그를 반드시 닫아야 한다는 점은 XML과 닮아있다.
바벨은 JSX를 React.createElement() 호출로 컴파일하고 결국 다음과 같은 객체를 생성한다. (다음 구조는 단순화된 것이다.) 바벨의 트랜스파일링은 여기서 직접 확인해 볼 수 있다.

// JSX -> JavaScript 트랜스파일링 후 

const message = { 
  type: 'div', 
  props: { 
    className: 'hello', 
    onClick: someFunc, 
    children: <span>Hello World</span> 
  } 
};

 

03. 요소 렌더링

요소(element)

리액트의 '요소'는 리액트 앱에서 가장 작은 단위(Building Block)에 해당한다. 요소는 컴포넌트의 구성요소라고도 할 수 있다. 리액트의 요소는 브라우저의 DOM 요소와 달리 일반 객체이며(plain object)이다. 따라서 생성하는 비용이 크지 않다. React DOM은 각 리액트 요소와 DOM이 일치하도록 DOM을 관리한다.

요소를 DOM에 렌더하려면

부모 요소에 자식요소를 추가하고 싶다면 부모 요소와 자식 요소를 ReactDom.render()에 전달하면 된다. React DOM은 해당 요소와 자식 요소를 이전의 것과 비교해서, 변경된 부분만 새로 그려준다. 전체 UI를 다시 생성하고 렌더하도록 코드를 작성하더라도 React DOM 덕분에 변경된 부분만 업데이트한다.

// index.html 

<div id="root"></div> 

// app.js 

function tick() { 
  const element = ( 
    <div> 
      <h1>Hello, world!</h1> 
      <h2>It is {new Date().toLocaleTimeString()}.</h2> 
    </div> 
  );
  
  ReactDOM.render(element, document.getElementById('root')); 
  {/* ReactDOM.render(자식요소, 부모요소); */} 
} 

setInterval(tick, 1000);

리액트의 요소는 불변 객체(immutable object)이다. 따라서 요소를 생성한 이후에 업데이트하려면 새로운 요소를 생성해서 ReactDom.render()에 전달하거나 이 코드를 유상태 컴포넌트(Stateful Component)로 캡슐화 해야한다. (이후에 나오는 05. 상태와 생명주기 참조)

 

04. 컴포넌트와 Props

컴포넌트(component)

리액트 앱에서는 버튼, 폼, 다이얼로그, 화면 등의 모든 것들을 컴포넌트로 표현한다. 컴포넌트는 UI를 독립적이고 재사용 가능한 조각으로 나누고, 각 조각을 개별적으로 다룰 수 있게 해준다. 일반적으로 React 앱은 최상위에 단 하나의 <App> 컴포넌트를 갖는다. UI 일부가 여러 번 사용되거나(<Button>), UI 일부가 자체적으로 복잡한 경우(<OrderList>)에는 내부에서 별도의 컴포넌트를 추출하는 것이 좋다.

속성(props)

React 요소는 앞서 살펴본 것처럼 <div>와 같은 DOM 태그로 나타낼 수도 있고, 직접 만든 사용자정의 컴포넌트로 나타낼 수도 있다. 아래 예시코드에는 <Welcome>이라는 사용자정의 컴포넌트를 사용하고 있다. React는 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX attribute와 자식을 단일 객체로서 해당 컴포넌트에 전달한다. 이 객체를 “props”라고 한다. (props는 properties의 줄임말이다.) props는 읽기 전용으로 그 값을 절대! 수정해서는 안된다. 또한, props의 네이밍은 어떤 문맥에서 사용되는지 보다 컴포넌트 자체의 관점에서 짓는 것이 좋다.

// 전달받은 객체  props: { name: 'Sara' } 

function Welcome(props) { 
  return <h1>Hello, {props.name}</h1>;   
} 

// React 요소 - 사용자 정의 컴포넌트 

const element = <Welcome name="Sara" />; 

ReactDOM.render( element, document.getElementById('root') );

함수 컴포넌트와 클래스 컴포넌트

한편, 컴포넌트를 자바스크립트의 함수로 작성하면 함수 컴포넌트(Function Component), 자바스크립트의 클래스로 작성하면 클래스 컴포넌트(Class Component)라고 한다. 함수 컴포넌트를 클래스 컴포넌트로 변환하는 방법은 다음과 같다.

1. 동일한 이름의 클래스를 작성하되, React.Component를 extends해서 상속받는다.
2. render() 메서드를 추가한다. (render()는 React.Component의 하위 클래스에서 반드시 정의해야 하는 메서드이다.)
3. 함수의 본문을 render() 메서드 안으로 옮긴다.
4. props를 this.props로 변경한다.

함수 컴포넌트와 클래스 컴포넌트는 각각 추가 기능이 있긴 하지만, 아래의 두 컴포넌트는 동일하다고 볼 수 있다. 둘 중 어떤 방식으로 작성하건 컴포넌트의 이름은 항상 대문자로 작성해야 한다. 소문자로 작성하면 React가 DOM 태그로 인식하기 때문이다.

// 함수 컴포넌트 정의 

function Welcome(props) { 
  return <h1>Hello, {props.name}</h1>; 
} 

// 클래스 컴포넌트 정의 

class Welcome extends React.Component { 
  render() { 
    return <h1>Hello, {this.props.name}</h1>; 
  } 
}

클래스 컴포넌트에서 render 메서드는 업데이트가 발생할 때마다 호출된다. 이 때, 같은 DOM 노드에 렌더해주는 한, 클래스의 같은 인스턴스를 계속 사용한다. 이렇게 딱 하나의 인스턴스를 계속해서 사용하는 것은 지역 상태(Local State)와 생명주기 메서드(Lifecycle Method)와 같은 기능을 사용할 수 있게 해준다.

props가 전달되는 것을 보여주는 시각자료 (출처: Facebook Develops의 "Introduction to React.js" 영상)

 

05. 상태와 생명주기

상태(state)

state는 props와 유사하지만, private이라는 점과 컴포넌트가 완전히 컨트롤한다는 점이 다르다. props 대신 state를 쓰면 <Clock />과 같은 컴포넌트가 상태를 갖고, 업데이트를 스스로 알아서 하도록 만들 수 있다. props를 state로 바꾸는 방법은 다음과 같다.

1. render() 메서드 안에 있는 this.props.sth를 this.state.sth로 바꾼다.
2. 초기 state를 설정하는 constructor를 추가한다.
3. 컴포넌트에서 prop을 삭제한다.
// props 사용 

function Clock(props) { 
  return ( 
    <div> 
      <h1>Hello, world!</h1> 
      <h2>It is {props.date.toLocaleTimeString()}.</h2> 
    </div> 
  ); 
} 
  
function tick() { 
  ReactDOM.render( 
    <Clock date={new Date()} />, 
    document.getElementById('root') 
  ); 
}
// state 사용 

class Clock extends React.Component { 
  constructor(props) { 
    super(props); 
    this.state = {date: new Date()}; 
  } 
  
  render() { 
    return ( 
      <div> 
        <h1>Hello, world!</h1> 
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2> 
      </div> 
    ); 
  } 
} 

ReactDOM.render( 
  <Clock />, 
  document.getElementById('root') 
);

State는 local 또는 캡슐화(encapsulation)라고도 불린다. 그 이유는 state를 갖고 있는 컴포넌트 본인 외에는, 다른 어떤 컴포넌트에서도 state에 접근할 수 없기 때문이다. 어느 state 든 간에 항상 어느 하나의 컴포넌트에게 속해있고, 이 state로부터 파생된 데이터나 UI는 자신보다 구조적으로 하위에 있는 컴포넌트에만 영향을 미치게 된다.
부모나 자식 컴포넌트 입장에서는 이 컴포넌트가 유상태(stateful)인지 또는 무상태(stateless)인지 알 수 없다. 물론 컴포넌트는 자신의 state를 자식 컴포넌트에게 props로 전달할 수 있다. 하지만 자식 컴포넌트는 전달받은 props가 부모의 state로부터 왔는지, props에서 왔는지, 아니면 아예 수동으로 입력한 값인지 알 수 없다. 이러한 데이터의 흐름을 '단방향식(unidirectional)' 또는 '하향식(top-down)'이라고 한다.
참고로 React 앱에서는 컴포넌트의 상태 여부(stateful vs stateless)는 시간이 지남에 따라 변할 수 있는 구현 세부사항 정도로 간주한다. 유상태 컴포넌트 안에서 무상태 컴포넌트를 사용할 수도, 무상태 컴포넌트 안에서 유상태 컴포넌트를 사용할 수도 있다.

상태를 바르게 사용하려면

State를 올바르게 사용하기 위해서는 지켜야 할 몇 가지 사항이 있다. 우선, state를 직접 수정하는 대신 setState()를 사용해야 한다. state를 수정하면 컴포넌트를 리렌더되지 않기 때문에 반드시 setState()를 사용해야 한다. 또, 비동기적인 업데이트에 대비하여 this.state나 this.props를 바로 사용하는 대신 (state, props)를 인자로 전달하는 함수를 작성해야 한다.

// 틀린 예시 🙅🏻‍♀️ 

this.state.comment = 'Hello'; 

// 올바른 예시 🙆🏻‍♀️ 

this.setState({comment: 'Hello'}); 
// 틀린 예시 🙅🏻‍♀️ 

this.setState({ counter: this.state.counter + this.props.increment, }); 

// 올바른 예시 🙆🏻‍♀️ 

this.setState((state, props) => ({ counter: state.counter + props.increment }));

생명주기 메서드(Lifecycle Method)

리액트에서는 컴포넌트가 처음 DOM에 렌더링 되는 시점을 '마운트(Mount)', 컴포넌트가 생성한 DOM이 삭제되는 시점을 '언마운트(Unmount)라고 한다. 클래스 컴포넌트에서는 컴포넌트가 'Mount'되거나 'Unmount' 되는 시점에 특정 코드가 동작할 수 있도록하는 생명주기 메서드를 사용할 수 있다. 생명주기 메서드는 컴포넌트가 삭제될 때 해당 컴포넌트가 사용 중이던 리소스를 확보하는 작업에도 유용하게 사용할 수 있다. 각 메서드별 자세한 설명은 여기서 확인할 수 있다.

마운트 업데이트 언마운트
컴포넌트의 인스턴스가 생성되어 DOM에 삽입시

1. constructor()
2. static getDerivedStateFromProps()
3. render()
4. componentDidMount()
props 또는 state가 변경시

1. static getDerivedStateFromProps()
2. shouldComponentUpdate()
3. render()
4. getSnapshotBeforeUpdate()
5. componentDidUpdate()
컴포넌트가 DOM 상에서 제거시

1. componentWillUnmount()




출처: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

아래 코드 예시를 보고 생명주기 메서드의 전체 사이클을 파악해보자.

1. ReactDOM.render()의 인자로 <Clock />컴포넌트 전달하면 React는 컴포넌트의 constructor를 호출한다.
2. 이어서 바로 React는 render()를 호출한다. React는 Clock의 렌더링 결과를 일치시키기 위해 DOM을 업데이트한다.
3. React는 업데이트를 마치면 componentDidMount()를 호출한다. (setInterval의 타이머를 설정하도록 브라우저에 요청)
4. 브라우저가 tick() 메서드 호출하면 tick()안에서 setState()를 호출된다.
5. setState()호출에 따라 React가 자동으로 render()를 호출하고 DOM을 업데이트한다.
6. 컴포넌트가 DOM으로부터 한 번이라도 삭제된 적이 있다면 React는 타이머를 멈추기 위해 componentWillUnmount() 를 호출한다.
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> ); } } ReactDOM.render( <Clock />, document.getElementById('root') );

 

06. 이벤트 핸들링

이벤트 핸들링 문법

리액트에서의 이벤트 핸들링 방식은 몇 가지 문법 차이를 제외하고는 DOM 요소의 이벤트 핸들링 방식과 크게 다르지 않다. 문법 차이만 짚고 넘어가자.

1. React에서는 소문자(onclick) 대신에 카멜케이스(onClick)로 쓴다.
2. JSX에서는 문자열(onclikc="func()") 대신에 함수(onClick={func})를 전달한다.
3. DOM 요소 생성 후 리스너를 추가하기 위해 addEventListener를 호출할 필요가 없다.

이벤트 객체 e (또는 합성 이벤트, Synthetic Event)는 W3C 명세에 따르고 있어 브라우저 호환성에 대해 걱정할 필요는 없다. 단, 리액트에서의 이벤트는 브라우저 고유 이벤트와 정확히 똑같이 동작하지는 않는다고 한다.