리액트 - 주요 개념

WooBuntu·2021년 3월 24일
0

알고 쓰자 리액트

목록 보기
1/11

리액트 공식문서

Hello World

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

ReactDOM.render()

https://ko.reactjs.org/docs/react-dom.html#render

ReactDOM.render(element, container[, callback])
  • 결국 리액트가 하는 일은 React element를 container DOM에 렌더링하는 것

  • 해당 React엘리먼트가 이전에 container 내부에 렌더링된 적이 있다면, 이전 React element와 비교하여 변경이 필요한 DOM만을 변경한다.

    • React element를 비교하는 것은 React의 DOM diffing 알고리즘으로 수행한다.
  • callback이 인자로 전달되면 컴포넌트가 렌더링되거나 업데이트된 후 실행된다.

JSX

JSX는 자바스크립트 객체(React element)이다.

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

바벨은 위와 같은 JSX표현식을 다음과 같이 React.createElement()호출로 컴파일한다.

const element = React.createElement(
  'h1', // type
  {className: 'greeting'}, // [props]
  'Hello, world!' // [...children]
);

React.createElement의 호출로 다음과 유사한 자바스크립트 객체, 즉 React element가 생성된다.

const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

이와 같이 JSX는 정규 자바스크립트 함수(React.createElement)의 호출을 통해 자바스크립트 객체(React element)로 평가되는 자바스크립트 표현식이다

또한, React element는 생성된 이후 자식이나 속성을 변경할 수 없는 불변객체이다.

결국 리액트가 하는 것은 이 자바스크립트 객체를 읽어서 가상 DOM을 만들고 DOM diffing 알고리즘을 통해 최소한의 DOM조작만을 하는 것이다.

인젝션을 방지하는 JSX

const title = response.potentiallyMaliciousInput;
// 이것은 안전합니다.
const element = <h1>{title}</h1>;
  • 위와 같이 사용자의 입력값을 JSX안에 사용하는 것은 안전하다

  • React DOM은 기본적으로 렌더링하기 이전에 JSX에 삽입된 모든 값을 escape한다.(escape...?)

    아마도 React.createElement에 넘겨줄 인자값으로 변환하는 과정에서 일종의 validation이 일어난다는 것 같은데 무슨 소린지 모르겠다

  • 그렇기 때문에(?) 애플리케이션에는 명시적으로 작성된 내용 외에는 포함될 수가 없다

  • 모든 항목(?)은 렌더링되기 전에 문자열로 변환된다.

  • 즉, XSS(cross-site-scripting)공격으로부터 안전하다.

엘리먼트 렌더링

  • JSX가 컴파일된 결과인 React element는 리액트 애플리케이션을 구성하는 최소 단위이다.
    (컴포넌트는 React element보다 상위의 요소이다)

  • 이 React element는 불변 객체이기 때문에 UI를 업데이트하고자 한다면 새로운 React element를 생성하여 이를 ReactDOM.render로 전달하는 수밖에 없다.

컴포넌트와 props

컴포넌트는 props를 입력값으로 받아 React element를 반환하는, 일종의 자바스크립트 함수이다.

함수형 컴포넌트

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

클래스형 컴포넌트

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

컴포넌트 렌더링

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

리액트는 위와 같은 사용자 정의 컴포넌트를 발견하면 속성(name)과 자식(여긴 없음)을 해당 컴포넌트에 단일 객체(props)로 전달하는 것

// props
{ name: "Sara" };
  • props의 이름은 해당 props를 사용할 컴포넌트의 관점에서 짓는 것이 좋다

컴포넌트는 순수 함수여야 한다.

인자값(props) 자체를 수정하지 않고, 항상 동일한 입력에 대해 동일한 결과값을 반환해야 한다는 의미이다.

State와 Lifecycle

Lifecycle

State

state의 업데이트는 비동기적으로 작동한다.

  • 한 이벤트로부터 여러 개의 state가 갱신될 수 있으므로, state하나가 바뀔 때마다 동기적으로 DOM을 업데이트하면 성능상 좋지 못하다.

  • 따라서 한 이벤트로부터 파생된 state의 변동들을 가상 DOM에 한꺼번에 반영해 실제 DOM의 조작 횟수를 줄이기 위해서 state의 업데이트는 비동기적으로 처리한다.

this.setState((state, props) => ({
  // 인자로 들어오는 state은 prevState값
  // 인자로 들어오는 props는 갱신이 완료된 props
  counter: state.counter + props.increment
}));

setState는 얕은 병합의 방식으로 작동한다.

  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }
  • 위와 같이 state에 복수의 변수가 포함되어 있더라도 아래처럼 독립적으로 변수를 갱신할 수 있다.
  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }
  • 이는 Object.assign 혹은 전개 연산자 등으로 얕은 복사(병합)하는 원리로 작동하기 때문

이벤트 처리

HTML과 JSX의 차이

이벤트 핸들러 등록

  • HTML에서의 이벤트 핸들러 등록(문자열)
<button onclick="activateLasers()">
  Activate Lasers
</button>
  • JSX에서의 이벤트 핸들러 등록
<button onClick={activateLasers}>
  Activate Lasers
</button>

기본 동작 방지

  • HTML(false반환)
<a href="#" onclick="console.log('The link was clicked.'); return false">
  Click me
</a>
  • JSX(preventDefault 명시적 호출)
function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

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

인자로 들어오는 e는 합성 이벤트(synthetic event)이다.

조건부 렌더링

render() {
  const count = 0;
  return (
    <div>
      { count && <h1>Messages: {count}</h1>}
    </div>
    // <div>0</div>
  );
}

위와 같이 false로 평가될 수 있는 표현식을 반환할 때는 주의해야 한다.

리스트와 Key

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

리액트에서 엘리먼트 리스트를 렌더링하기 위해서는 위와 같이 key라는 문자열 속성을 설정해주어야 한다.

이렇게 엘리먼트 리스트에 key를 설정해주어야 하는 이유는 각 엘리먼트에 고유성을 부여하여 변경, 추가, 삭제할 엘리먼트를 효율적으로 식별하기 위함이다.

즉, 해당 데이터를 고유하게 식별할 수 있는 문자열을 key로 사용해야 하기에 보통은 해당 데이터의 ID를 사용한다.

리스트의 인덱스를 key로도 사용할 수 있지만, 만약 데이터의 순서가 바뀔 수 있는 경우 성능 저하나 state와 관련된 문제가 발생할 수 있기에 가능한 지양해야 한다.
(명시적으로 key를 지정해주지 않을 경우 리액트는 index를key로 사용한다)

Form

HTML의 Form 엘리먼트는 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트한다.

반면 React는 Form엘리먼트의 state까지도 컴포넌트의 state로써 관리한다.

이렇게 React에 의해 값이 제어되는 Form엘리먼트를 controlled component라고 한다.

  • validation과 방문한 필드 추적, form submit 처리 등을 위한 라이브러리로 Formik이 있다.

textarea

  • HTML : 텍스트를 자식으로 정의
<textarea>
  Hello there, this is some text in a text area
</textarea>
  • JSX : value와 onChange 속성 사용
<textarea value={this.state.value} onChange={this.handleChange} />

select

  • HTML
<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>
  • JSX
<select value={this.state.value} onChange={this.handleChange}>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>
  • JSX(multiple 옵션 허용)
<select multiple={true} value={['B', 'C']}>

file input

  • HTML, JSX

    값이 읽기 전용이기 때문에 uncontrolled-component이다.

    File API로 조작할 수 있다.

<input type="file" />

컴포넌트 합성

범용적인 '박스' 역할을 하는 컴포넌트는 어떤 엘리먼트를 자식으로 가질 지 사전에 특정할 수 없는 경우도 있다. 이러한 컴퍼넌트에서는 자식 엘리먼트를 children prop으로 전달하는 것이 좋다.

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}
// FancyBorder의 자식 엘리먼트들은 명시적으로 props로 넘겨주지 않아도 아래와 같이 참조할 수 있다.
function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}
  • 물론 아래와 같이 props에 명시적으로 엘리먼트를 전달하는 것도 가능하다.
function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

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

컴포넌트 특수화

한 컴포넌트가 어떤 컴포넌트의 보다 특수한 경우인 경우 역시 컴포넌트 합성으로 구현할 수 있다.
(공식 문서의 뉘앙스로 봐서는 상속과는 명백히 구분되는 개념인 모양이다)

  • 함수형 컴포넌트
function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </FancyBorder>
  );
}

function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thank you for visiting our spacecraft!" />
  );
}
  • 클래스형 컴포넌트
function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </FancyBorder>
  );
}

class SignUpDialog extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = {login: ''};
  }

  render() {
    return (
      <Dialog title="Mars Exploration Program"
              message="How should we refer to you?">
        <input value={this.state.login}
               onChange={this.handleChange} />
        <button onClick={this.handleSignUp}>
          Sign Me Up!
        </button>
      </Dialog>
    );
  }

  handleChange(e) {
    this.setState({login: e.target.value});
  }

  handleSignUp() {
    alert(`Welcome aboard, ${this.state.login}!`);
  }

리액트 애플리케이션 디자인하기

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

컴포넌트도 함수와 마찬가지로 오직 한 가지 기능만을 가지도록 구현하는 것이 바람직하다.

2. static한 버전으로 구현

사용자 상호작용 없이 렌더링만 가능한 버전으로 구현

이 단계에서는 컴포넌트 간에 데이터를 전달하기 위해 오직 props만 사용한다.

state는 오직 상호작용을 위해서만 사용해야 한다.

앞서 만든 컴포넌트 계층 구조에서 bottom-up, 즉 가장 하위의 컴포넌트부터 구현하는 것이 권장된다.

3. state를 최소한으로 관리할 수 있는 방안 모색

state로 관리되어야 할 데이터

  1. 부모로부터 props로 전달되지 않을 것

  2. 사용자와의 상호 작용으로 변해야 할 것

  3. 컴포넌트 내부의 다른 state나 props를 통해 계산할 수 없을 것

4. state가 위치해야 할 계층 모색

3단계에서 찾아낸 state의 최소 집합이 각각 어떤 컴포넌트에서 관리되어야 할 지 결정

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

  2. 해당되는 컴포넌트들을 자식 컴포넌트로 가지는 부모 컴포넌트 확인

5. 역방향 데이터 흐름 추가하기

사용자와의 상호 작용이 state를 관리하는 컴포넌트보다 하위 컴포넌트에서 일어난다면, 해당 컴포넌트까지 setState를 props로 내려줘서 해당 컴포넌트가 부모 컴포넌트의 state를 변경할 수 있도록 구현

요약

  1. 리액트는 container에 React element를 렌더링한다.

  2. JSX가 컴파일되면 React element가 된다.

  3. 이 React element를 기능 단위로 조합한 것이 컴포넌트이다.

  4. 리액트는 데이터의 흐름을 명시적으로 보이게끔 하기 위해 단방향 데이터 흐름을 갖는다.

  5. 단방향 데이터 흐름에서 부모 컴포넌트는 자식 컴포넌트로 props를 통해 데이터를 내려준다.

  6. 사용자와의 상호 작용으로 UI가 변해야 할 경우 관련된 데이터를 state로 관리한다.

0개의 댓글