본문 바로가기
React

리액트 공부한 내용 정리

by ttum 2020. 1. 31.

React 공식 문서(주요 개념) 보면서 내가 기록해두고 싶은 것들만 정리한 내용

JSX 소개

JSX는 Javascript를 확장한 문법이다.

생긴 것은 꼭 html과 JavaScript어느 중간 사이로 보이지만 따지자면 JavaScript에 조금 더 가깝다.

(그렇기 때문에 JavaScript와 같이 camelCase를 사용한다.)

 

JSX는 표현식이다. 때문에 if문이나 for loop등을 사용할 수 있다. 

컴파일이 끝나면 JSX 표현식은 정규 JavaScript 함수가 되고, JavaScript 객체로 인식된다.

아래와 같이 if문을 사용하는 것이 가능하다!

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;
  }
  return <h1>Hello, Stranger.</h1>;
}

엘리먼트 렌더링

엘리먼트(element)는 컴포넌트(component)의 구성요소이다. 아래 코드가 엘리먼트의 한 예시이다.

const element = <h1>Hello, world</h1>;

 

HTML파일 어딘가에 아래와 같은 <div>가 있다고 가정해보자. 

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

이 div태그 안의 모든 엘리먼트를 React DOM에서 관리하기 때문에 이것을 루트 DOM 노드라고 부른다.

React로 구현된 어플리케이션은 일반적으로 하나의 루트 DOM노드가 있다. React를 기존 앱에 통합하려는 경우 원하는 만큼 많은 수의 독립된 루트 DOM노드를 가질 수 있다.

 

React 엘리먼트를 루트 DOM노드에 렌더링하려면 둘다 ReactDOM.render()로 전달하면 된다.

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));

엘리먼트는 불변 객체이다. 따라 엘리먼트를 생성한 이후에는 해당 엘리먼트의 자식이나 속성을 변경할 수 없다.

엘리먼트는 영화에서 하나의 프레임과 같이 특정 시점의 UI를 보여준다.

 

React는 변경된 부분만 업데이트 한다. React DOM은 해당 엘리먼트와 그 자식 엘리먼트를 이전의 엘리먼트와 비교하고 DOM을 원하는 상태로 만드는데 필요한 경우에만 DOM을 업데이트한다. (그래서 매번 페이지를 새로고침할 필요 없이 엘리먼트만 업데이트 된다.)


Component와 Props

컴포넌트를 통해 UI를 재사용 가능한 여러개의 조각으로 나누고, 각 조각을 개별적으로 살펴볼 수 있다.

 

컴포넌트는 무엇인가?

컴포넌트는 JavaScript 함수와 유사하다. "props"라는 입력(함수의 파라미터와 비슷한 것)을 받아 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환한다.

 

어떻게 만드는가?

두 가지 방법이 있다. 함수 또는 클래스를 통해 컴포넌트를 만들 수 있다.

1. 함수 컴포넌트

JavaScript 함수로 컴포넌트를 작성할 수 있다. 함수형 컴포넌트는 props라는 객체 인자를 받은 후 React 엘리먼트를 반환한다.

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

2. 클래스 컴포넌트

ES6 Class를 사용하여 컴포넌트를 정의하는 방법이다.

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

React관점에서 봤을 때 두 가지 유형의 컴포넌트는 동일하다. 그러나 클래스 컴포넌트에는 몇가지 추가 기능들이 있다.(후에 나올 state를 추가하는 등의 기능은 함수형에서는 사용할 수 없고 클래스 컴포넌트에서만 사용할 수 있다.)

 

props는 읽기 전용이다.

함수 컴포넌트나 클래스 컴포넌트 모두 컴포넌트 자체 props를 수정해서는 안된다.

function sum(a, b) {
  return a + b;
}

위 함수는 순수 함수이다. 순수함수는 입력값을 바꾸려 하지 않고 항상 동일한 입력값에 대해 동일한 결과를 반환한다.

반면 아래의 함수는 자신의 입력값을 변경하므로 순수 함수가 아니다.

function withdraw(account, amount) {
  account.total -= amount;
}

모든 React 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 한다.


State와 생명주기

함수에서 클래스로 변환하기

(방법은 여기 참고)

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

render 함수는 업데이트가 발생할때마다 호출된다.

그러나 우리가 같은 DOM 노드에 <Clock/>을 렌더링 하는동안은 하나의 Clock 클래스 객체만 사용된다. (매번 새로운 객체가 생성되는 것이 아니라는 말인 듯 하다.)

이러한 특징은 우리가 state나 생명주기 등의 추가적인 함수를 사용할 수 있게 한다.

 

생명주기 메서드를 클래스에 추가하기

컴포넌트가 DOM에 렌더링 되는 것을 "마운팅"이라고 한다.

componentDidMount(): 만들어 놓은 컴포넌트가 DOM에 삽입될 때 호출된다. (처음 DOM에 올라갈 때 한 번 호출된다.)

state가 바뀌게 되면 render함수가 다시 호출된다.

 

State 올바르게 사용하기 (중요!)

  1. State를 직접 수정하지 않기
    이럴 경우에는 컴포넌트를 다시 렌더링하지 않는다. 대신 꼭 setState를 사용해야 한다.
    this.state를 직접 지정할 수 있는 유일한 공간은 바로 constructor이다.
  2. State 업데이트는 비동기적일 수 있음
    React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다.
    this.props와 this.state가 비동기적으로 업데이트될 수 있기 때문에 다음 state를 계산할 때 해당 값에 의존해서는 안된다.
  3. State 업데이트는 병합됨
    state는 다양한 독립변수를 가지고 있을 수 있다.

    그렇지만 setState로 업데이트를 할때 매번 모든 변수들을 다 열거해주는 대신, 업데이트하고자 하는 변수만 적어서 독립적으로 업데이트 할 수 있다. 알아서 병합 되기 때문이다. (병합은 얉은 병합으로 이루어진다.)

* 2번 State 업데이트가 비동기적일 수 있다는 것에 대한 추가적인 설명이다.

아래 코드의 경우에는 카운터 업데이트가 실패할 수 있다. (state와 props가 비동기적으로 업데이트 될 수 있기 때문)

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

이를 위해서 다른 형태의 setState()를 사용해야 한다.

아래 함수의 매개변수는 각각 이전 함수의 state업데이트가 적용된 시점의 props를 인자로 받아들인다.

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

화살표 함수 대신 일반적인 함수로 표현해도 된다.

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

데이터는 아래로 흐른다

이런 구조를 일반적으로 "하향식(top-down)" 또는 "단방향식" 데이터 흐름이라고 한다.

모든 state는 항상 특정 컴포넌트가 소유하며 그 state에서 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 "아래"에 있는 컴포넌트에만 영향을 미친다.

또한 컴포넌트끼리는 완전히 독립적이다.


이벤트 처리하기

* 이벤트 처리하기는 javascript에 대해 더 깊이 공부한 후에 다시 정리하는 것이 좋을 듯 하다.

 

리액트에서 onClick으로 함수를 호출할 때는 다음과 같다. (activateLasers()가 아님!)

<button onClick={activateLasers}>
  Activate Lasers
</button>

html의 경우는 다음과 같다.

<button onclick="activateLasers()">
  Activate Lasers
</button>

 

또한 리액트에서는 false를 반환해도 기본 동작을 방지할 수 없다. 반드시 preventDefault를 명시적으로 호출해야 한다.

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

React를 사용할 때에는 엘리먼트가 처음 렌더링 될 때 리스너를 제공하면 되며, DOM 엘리먼트가 생성된 후에 리스너를 추가하기 위해 addEventListener를 호출할 필요가 없다.

 

*

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 콜백에서 `this`가 작동하려면 아래와 같이 바인딩 해주어야 합니다.
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(state => ({
      isToggleOn: !state.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

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

JSX 콜백 안에서 this의 의미에 대해 주의 해야 한다. JavaScript에서 클래스 메서드는 기본적으로 바인딩되어 있지 않는다. this.handleClick을 바인딩하지 않고 onClick에 전달하였다면, 함수가 실제 호출될 때 this는 undefined가 된다.

 

일반적으로 onClick={this.handleClick}과 같이 뒤에 ()를 사용하지 않고 메서드를 참조할 경우, 해당 메서드를 바인딩 해야 한다.

 

만약 bind를 호출하는 것이 불편하다면, 이를 해결할 수 있는 두 가지 방법이 있다.

1. 실험적인 퍼블릭 클래스 필드 문법

class LoggingButton extends React.Component {
  // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
  // 주의: 이 문법은 *실험적인* 문법입니다.
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

Create React App에서 기본적으로 설정되어 있는 문법이다.

"실험적인 퍼블릭 클래스 필드 문법"에 대해 구체적으로는 알 수 없으나 흔히 사용하는 화살표 함수를 변수에 담은 경우를 말하는 것 같다. 이 경우에는 handleClick 내에 this가 바인딩되도록 한다. 

 

2. 콜백에 화살표 함수를 사용하기 (비권장☹️)

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다.
    return (
      <button onClick={(e) => this.handleClick(e)}>
        Click me
      </button>
    );
  }
}

* 이 문법의 문제점은 LoginButton이 렌더링 될 때마다 ㅏ다른 콜백이 생성된다는 것이다. 대부분의 경우 크게 문제되는 일은 없지만, 콜백이 하위 컴포넌트에 props로서 전달된다면 그 컴포넌트들은 추가로 다시 렌더링을 수행할 수 있다. 이런 종류의 성능 문제를 피하고자, 생성자 안에서 바인딩하거나 클래스 필드 문법을 사용하는 것을 권장한다.


조건부 렌더링

JavaScript처럼 if문을 사용해서 조건에 따라 렌더링할 수 있다.

function Greeting(props) {
  const isLoggedIn = props.isLoggedIn;
  if (isLoggedIn) {
    return <UserGreeting />;
  }
  return <GuestGreeting />;
}

 위의 방법도 있지만 인라인으로 간단하게 표현하는 방법도 있다.

논리 && 연산자로 If를 인라인으로 표현하기

JSX 안에는 중괄호를 이용해서 표현식을 포함할 수 있다. 그 안에 JavaScript의 논리 연산지 &&를 사용하면 쉽게 엘리먼트를 조건부로 넣을 수 있다.

아래 코드의 경우 unreadMessages의 개수가 0보다 클 때 'You have ... unread messages"라는 표시를 해준다.

function Mailbox(props) {
  const unreadMessages = props.unreadMessages;
  return (
    <div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&
        <h2>
          You have {unreadMessages.length} unread messages.
        </h2>
      }
    </div>
  );
}

const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
  <Mailbox unreadMessages={messages} />,
  document.getElementById('root')
);

 

조건부 연산자로 If-Else 구문 인라인으로 표현하기

앞의 경우와 마찬가지로 중괄호를 열고 그 안에 condition ? true : false 구문을 사용할 수 있다.

render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
    </div>
  );
}

JavaScript와 마찬가지로, 가독성이 좋다고 생각하는 방식을 선택하면 된다. 또한 조건이 너무 복잡하다면 컴포넌트를 분리하기 좋을 때일 수 있다는 것을 잊지 말자.

 

컴포넌트가 렌더링하는 것을 막기

가끔 다른 컴포넌트에 의해 렌더링될 때 컴포넌트 자체를 숨기고 싶을 때가 있다.

이때는 렌더링 결과를 출력하는 대신 null을 반환하면 해결할 수 있다.

function WarningBanner(props) {
  if (!props.warn) {
    return null;
  }

  return (
    <div className="warning">
      Warning!
    </div>
  );
}

* 컴포넌트의 render 메서드로부터 null을 반환하는 것은 생명주기 메서드 호출에 영향을 주지 않는다.

따라서 componentDidUpdate는 계속해서 호출된다.


리스트와 Key

엘리먼트 리스트를 만들 때에는 꼭 key를 넣어줘야 한다.

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map(number => (
    <li key={number.toString()}>{number}</li>
  ));

key

key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하도록 한다.

key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 한다.

(키는 형제 사이에서만 고유하면 되고 전 범위에서 고유할 필요는 없다.)

 

대부분 id를 key로 사용하며, id가 없을 경우에 최후의 수단으로 항목의 인덱스로 key를 사용할 수 있다.

인덱스를 key로 사용하는 것은 권장되지 않는다. (여기 자세한 설명이 있다.)

(물론 리스트가 불변하는 경우나 아이디가 필요없을 때 인덱스로 사용해도 되는 경우들이 있긴 하다. )

 

* 키를 사용하는 방법 두 가지

Better(인덱스로 사용하는 것보다는 나은 방법)

permanent하고 unique한 속성을 사용하는 하는 방법이 있다. 예를들면 글로벌 인덱스를 사용하는 방법이 있을 수 있다.

todoCounter = 1;
function createNewTodo(text) {
  return {
    completed: false,
    id: todoCounter++,
    text
  }
}

Much better(위의 방법보다도 더 나은 방법)

shortid를 사용하는 방법이 있다. 이것은 빠르게 'short non-sequential url-friendly unique'한 id를 만들어준다.

const shortid = require('shortid');

function createNewTodo(text) {
  return {
    completed: false,
    id: shortid.generate(), // 대략 이런 식으로 형성된다( ex: PPBqWA9 ) 
    text
  }
}

경험상 map() 함수 내부에 있는 엘리먼트에 key를 넣어 주는 게 좋습니다.

(나의 경험이 아닌 리액트 독스 작성자의 경험!!)

* 또한 map이 자꾸 중첩된다면 컴포넌트로 따로 빼는 것이 좋다.


폼(Form)

제어 컴포넌트 (Controlled Component)

HTML에서는 <input>, <textarea>, <select>등의 폼 엘리먼트들은 일반적으로 자기 자신의 상태를 관리하고 유저의 입력에 따라 업데이트를 하기도 한다. 

React는 React state를 "신뢰 가능한 단일 출처(single source of truth)"로 만들어 이 두 작업을 합쳐두었다.

때문에 폼을 관리하는 React 컴포넌트유저의 입력을 제어하는 일을 담당한다.

이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 "제어 컴포넌트"라고 한다.

 

파일 input 태그는 값이 읽기 전용이기 때문에 비제어 컴포넌트이다.

 

다중 입력 제어하기

하나의 handleInputChange함수로 여러 입력을 제어하는 예시다.

ES6의 computed property name 구문을 사용하고 있다. (setState부분)

class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isGoing: true,
      numberOfGuests: 2
    };

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Number of guests:
          <input
            name="numberOfGuests"
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}

 

제어되는 Input Null 값

제어컴포넌트에 value prop을 지정하면 사용자가 값을 변경할 수 없다. 만약 value를 설정했는데 여전히 수정이 가능하다면 실수로 그 값을 null이나 undefined로 준 것은 아닌지 확인해보면 된다.


State 끌어올리기

종종 동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야 할 필요가 있다. 이럴 때는 가장 가까운 공통조상으로 state를 끌어올리는 것이 좋다.

 

React 애플리케이션 안에서 변경이 일어나는 데이터에 대해서는 "진리의 원천(source of truth)"을 하나만 두어야 한다.

보통의 경우 state는 렌더링에 그 값을 필요로 하는 컴포넌트에 먼저 추가된다. 그리고나서 다른 컴포넌트도 그 값이 필요하게 되면 그 값을 그들의 가장 가까운 공통 조상으로 끌어올리면 된다.

다른 컴포넌트 간에 존재하는 state를 동기화시키려고 노력하는 대신 하향식 데이터 흐름에 기대는 것을 추천한다.


구성 (Composition) vs 상속 (Inheritance)

React는 강력한 구성 모델을 가지고 있으며, 상속 대신 구성을 사용하여 컴포넌트 간에 코드를 재사용하는 것이 좋다.

 

컴포넌트에서 다른 컴포넌트 담기

props로 자식 컴포넌트에게 전달할 수 있는 것에는 제한이 없다. (원시 타입의 값, React 엘리먼트, 함수 등) 특히 React 엘리먼트는 단지 객체이기 때문에 다른 데이터처럼 prop으로 전달할 수 있다.

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

 

*JSX문법을 이용해서 className도 정의할 수 있다.

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

가령 위와같은 코드가 가능하다. props.color가 "blue"일 경우, className은 "FancyBorder FancyBorder-blue"가 된다.

이런 방법 외에 여러개의 클래스 이름을 넣고 선별적으로 넣고싶을 때 classnames라는 JavaScript utility를 사용할 수도 있다.

 

Facebook 개발자들도 React로 컴포넌트들을 만들면서 상속 계층 구조로 작성할만한 사례를 아직 찾지 못했다고 한다.

(* 구성(composition)과 상속(inheritance)의 차이는 더 공부해봐야 할 것 같다.)

 


React로 생각하기

단일책임원칙

하나의 컴포넌트는 한 가지 일을 하는게 이상적이라는 원칙이다. 하나의 컴포넌트가 커지게 된다면 이는 보다 작은 하위 컴포넌트로 분리되어야 한다.

* 한 가지 일을 한다는 것의 의미: 각 컴포넌트가 데이터 모델의 한 조각을 나타내도록 분리하기!

1단계: UI를 컴포넌트 계층구조로 나누기

위 그림과 같이 컴포넌트를 조각 조각 나누는 것을 말한다.

2단계: React로 정적인 버전 만들기

정적인 버전을 만들 때는 state를 사용하지 말자. state는 상호작용을 위해 사용하는 것이다. 2단계는 정적인 버전을 만드는 것이기 때문에 props를 이용하여 충분히 완성할 수 있다.

앱을 만들 때, 상부층 컴포넌트부터 만들고 하부층 컴포넌트를 만드는 하향식(top-down) 방법과 반대로 거슬러 올라가는 상향식(bottom-up)방법이 있다. 보통 하향식으로 만드는 것이 쉽지만 프로젝트가 커질수록 상향식으로 만들고 테스트를 작성하며 개발하는 것이 쉽다.

3단계: UI state에 대한 최소한의 표현 찾아내기

UI를 상호작용하게 만들려면 기반 데이터 모델을 변경할 수 있는 방법이 있어야 한다. React에서는 이것이 state를 통해 변경된다!

애플리케이션을 만들 때 애플리케이션에서 필요로하는 변경 가능한 state의 최소 집합을 생각해야 한다. 여기서 핵심은 중복배체원칙이다. 가령 todo아이템을 저장하는 배열을 state에 저장하고 todo의 길이를 또 저장하면 좋지 않다. 

어떤 것이 state가 되면 좋을지는 아래의 세가지 질문을 통해 결정할 수 있다.

1. 부모로부터 props를 통해 전달됩니까? => 그렇다면 확실히 state가 아니다.

2. 시간이 지나도 변하지 않는가? => 그렇다면 확실히 state가 아니다.

3. 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가? => 그렇다면 state가 아니다.(todo의 길이와 같은경우!)

4단계: State가 어디에 있어야 할 지 찾기

어떤 컴포넌트가 어느 state를 변경하거나 소유할지를 찾아야 한다. 이럴 때 아래의 내용들을 상기시켜보자.

1. state를 기반으로 렌더링하는 모든 컴포넌트 찾기

2. 공통 소유 컴포넌트를 찾기(계층 구조 내에서 특정 state가 있어야 하는 모든 컴포넌트들의 상위에 있는 하나의 컴포넌트 찾기)

3. 공통 혹은 더 상위에 있는 컴포넌트가 state를 가져야 한다.

4. state를 소유할 적절한 컴포넌트를 찾지 못했다면, state를 소유하는 컴포넌트를 하나 만들어서 공통 오너 컴포넌트의 상위 계층에 추가하기.

5단계: 역방향 데이터 흐름 추가하기

React는 전통적인 양방향 데이터 바인딩과 비교하면 더 많은 타이핑을 필요로 하지만 데이터 흐름을 명시적으로 보이게 만들어서 프로그램이 어떻게 동작하는지 파악하기 좋다.