개발 기록 남기기✍️

[React] 공식문서 정리 - 주요 개념(1) 본문

Front-End/React

[React] 공식문서 정리 - 주요 개념(1)

너해동물원 2023. 2. 1. 12:19
  • 이 글은 리액트 공식 문서를 읽으며 공식 문서를 읽는 습관을 들이고 리액트의 기본적인 개념과 문법을 익히기 위 작성하게 되었습니다.
  • 2-3번 정독할 예정으로, 추후 정리한 내용이 수정되거나 추가될 수 있습니다.
  • 리액트 공식 문서 ➡️ https://reactjs.org/docs/hello-world.html

2. JSX 소개

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

위의 문법은 JSX라 하며 JavaScript를 확장한 문법이다. UI가 어떻게 생겨야 하는지를 설명하기 위해 React와 함께 사용하는 것을 권장한다.

 

JSX는 React 엘리먼트를 생성한다.

 

JSX란?

React에서는 렌더링 로직이 UI 로직(이벤트가 처리되는 방식, 시간에 따라 state가 변하는 방식, 화면에 표시하기 위해 데이터가 준비되는 방식 등)과 연결된다.

 

React는 별도의 파일에 마크업과 로직을 넣어 기술을 분리하는 대신, 둘 다 포함하는 컴포넌트라는 유닛으로 관심사를 분한다.

 

 

JSX에 표현식 포함하기

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;

JSX 중괄호 안에는 유효한 모든 JavaScript 표현식을 넣을 수 있다. 또한 여러 줄로 나눠서 표현할 수 있는데, 자동 세미콜론 삽입을 피하려면 괄호로 묶는 것을 권장한다.

 

function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};

const element = (
  <h1>
    Hello, {formatName(user)}!
  </h1>
);

 

 

JSX도 표현식

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

즉, JSX를 if 구문 및 for loop 안에 사용하고, 변수에 할당하고, 인자로서 받아들이고, 함수로부터 반환할 수 있다.

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

 

 

JSX 속성 정의

어트리뷰트에 따옴표를 이용해 문자열 리터럴을 정의할 수 있다.

중괄호를 사용하여 어트리뷰트에 JavaScript 표현식을 삽입할 수도 있다.

const element1 = <a href="https://www.reactjs.org"> link </a>;
const element2 = <img src={user.avatarUrl}></img>;

 

🚨경고🚨
JSX는 HTML보다는 JavaScript에 가깝기 때문에, React DOM은 HTML 어트리뷰트 이름 대신 camelCase 프로퍼티 명명 규칙을 사용합니다.
예를 들어, JSX에서 class는 className가 되고 tabindex는 tabIndex가 된다.

 

 

JSX로 자식 정의

태그가 비어있다면 XML 처럼 />를 이용해 바로 닫아주어야 한다.

JSX 태그는 자식을 포함할 수 있다.

const element1 = <img src={user.avatarUrl} />;
const element2 = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);

 

 

JSX는 주입 공격을 방지한다.

JSX에 사용자 입력을 삽입하는 것은 안전하다.

const title = response.potentiallyMaliciousInput;
// 이것은 안전합니다.
const element = <h1>{title}</h1>;

기본적으로 React DOM은 JSX에 삽입된 모든 값을 렌더링하기 전에 이스케이프 하므로, 애플리케이션에서 명시적으로 작성되지 않은 내용은 주입되지 않는다. 모든 항목은 렌더링 되기 전에 문자열로 변환된다. 이런 특성으로 인해 XSS(cross-site-scripting) 공격을 방지할 수 있다.

 

XSS 공격
웹 응용프로그램에 존재하는 취약점을 기반으로 웹 서버와 클라이언트 간 통신 방식인 HTTP 프로토콜 동작과정 중에 발생한다. XSS 공격은 웹사이트 관리자가 아닌 이가 웹페이지에 악성 스크립트를 삽입할 수 있다.

 

 

JSX는 객체를 표현한다.

Babel은 JSX를 React.createElement() 호출로 컴파일한다.

다음 element1과 element2는 동일하다.

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

const element2 = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);

 

React.createElement()는 버그가 없는 코드를 작성하는데 도움이 되도록 몇 가지 검사를 수행하며, 기본적으로 다음과 같은 객체를 생성한다.

// 주의: 다음 구조는 단순화되었습니다.
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

 

이러한 객체를 React 엘리먼트라고 하며, 화면에서 보고 싶은 것을 나타내는 표현이라 생각하면 된다.

React는 이 객체를 읽어서 DOM을 구성하고 최신 상태로 유지하는데 사용한다.

 


3. 엘리먼트 렌더링

엘리먼트는 React 앱의 가장 작은 단위이다. 엘리먼트는 화면에 표시할 내용을 기술한다.

브라우저 DOM 엘리먼트와 달리 React 엘리먼트는 일반 객체이며 쉽게 생성할 수 있다.

React DOM은 React 엘리먼트와 일치하도록 DOM을 업데이트한다.

 

 

DOM에 엘리먼트 렌더링하기

HTML 파일 어딘가에 <div>가 있다고 가정해보자.

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

이 안에 들어가는 모든 엘리먼트를 React DOM에서 관리하기 때문에 이것을 root DOM 노드라고 부른다.

 

React로 구현된 애플리케이션은 일반적으로 하나의 루트 DOM 노드가 있다.

React 엘리먼트를 렌더링하기 위해서는 우선 DOM 엘리먼트를 ReactDOM.createRoot()에 전달한 다음, React 엘리먼트를 root.render()에 전달해야 한다.

 

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

 

React 엘리먼트는 불변 객체이다. 엘리먼트를 생성한 이후에는 해당 엘리먼트의 자식이나 속성을 변경할 수 없다. 엘리먼트는 특정 시점의 UI를 보여준다.

 

 

변경된 부분만 업데이트 하기

React DOM은 해당 엘리먼트와 그 자식 엘리먼트를 이전의 엘리먼트와 비교하고 DOM을 원하는 상태로 만드는 데 필요한 경우에만 DOM을 업데이트한다.

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

 

매초 UI를 다시 그리도록 엘리먼트를 만들었지만 React DOM은 내용이 변경된 텍스트 노드만 업데이트한다.


4. Component와 Props

개념적으로 컴포넌트는 JavaScript 함수와 유사하다.props라고 하는 임의의 입력을 받은 후, 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환한다.

 

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

 

 

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

컴포넌트를 정의하는 가장 간단한 방법은 JavaScript 함수를 작성하는 것이다.

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

props는 속성을 나타내는 데이터이다.

이 함수는 데이터를 가진 하나의 props 객체 인자를 받은 후 React 엘리먼트를 반환하므로 유효한 React 컴포넌트이다. 이러한 컴포넌트는 함수 컴포넌트라고 호칭한다.

 

또한 ES6 class를 사용하여 컴포넌트를 정의할 수 있다.

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

 

 

컴포넌트 렌더링

React 엘리먼트는 사용자 정의 컴포넌트로도 나타낼 수 있다.

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

 

React가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX 어트리뷰트와 자식을 해당 컴포넌트에 단일 객체로 전달한다. 이 객체를 props라고 한다.

 

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

const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <Welcome name="Sara" />;
root.render(element);
  1. <Welcome name="Sara" /> 엘리먼트로 root.render()를 호출한다.
  2. React는 {name: 'Sara'}를 props로 하여 Welcome 컴포넌트를 호출한다.
  3. Welcome 컴포넌트는 결과적으로 <h1>Hello, Sara</h1> 엘리먼트를 반환힌다.
  4. React DOM은 <h1>Hello, Sara</h1> 엘리먼트와 일치하도록 DOM을 효율적으로 업데이트힌다.
🚨주의 : 컴포넌트의 이름은 항상 대문자로 시작한다.

 

 

컴포넌트 합성

컴포넌트는 자신의 출력에 다른 컴포넌트를 참조할 수 있다.

React 앱에서는 버튼, 폼, 다이얼로그, 화면 등의 모든 것들이 흔히 컴포넌트로 표현된다.

 

예를 들어 Welcome을 여러 번 렌더링하는 App 컴포넌트를 만들 수 있다.

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

// 일반적으로 새 React 앱은 단일 App 컴포넌트를 가진다.
function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

 

.

 

 

컴포넌트 추출

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

 

Avatar 추출하기

function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

Avatar 는 자신이 Comment 내에서 렌더링 된다는 것을 알 필요가 없다. 따라서 props의 이름을 author에서 더욱 일반화된 user로 변경한다. props의 이름은 사용될 context가 아닌 컴포넌트 자체의 관점에서 짓는 것을 권장한다.

 

UserInfo 추출하기

function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}

 

컴포넌트화로 단순해진 Comment

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

 

UI 일부가 여러 번 사용되거나(Button, Panel, Avatar), UI 일부가 자체적으로 복잡한 경우(App, FeedStory, Comment)에는 별도의 컴포넌트로 만드는 것이 좋다.

 

 

props는 읽기 전용

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

 

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


5. State와 생명주기

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

  1. React.Component를 확장하는 동일한 이름의 ES6 class를 생성한다.
  2. render()라고 불리는 빈 메서드를 추가한다.
  3. 함수의 내용을 render() 메서드 안으로 옮긴다.
  4. render() 내용 안에 있는 props를 this.props로 변경한다.
  5. 남아있는 빈 함수 선언을 삭제한다.

state를 관리할 Clock 컴포넌트

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

setInterval(tick, 1000);

 

 

Clock 컴포넌트 클래스화 하기

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

Clock은 이제 함수가 아닌 클래스로 정의된다.

render 메서드는 업데이트가 발생할 때마다 호출되지만, 같은 DOM 노드로 <Clock />을 렌더링하는 경우 Clock 클래스의 단일 인스턴스만 사용된다. 이는 로컬 state와 생명주기 메서드와 같은 부가적인 기능을 사용할 수 있게 해준다.

 

 

클래스에 로컬 State 추가하기

세 단계에 걸쳐서 date를 props에서 state로 이동시킨다.

 

1. render() 메서드 안에 있는 this.props.date를 this.state.date로 변경한다.

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

 

2. 초기 this.state를 지정하는 class constructor를 추가한다.

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>
    );
  }
}

클래스 컴포넌트는 항상 props로 기본 constructor를 호출해야 한다.

 

3. <Clock /> 요소에서 date prop을 삭제한다.

root.render(<Clock />);

 

결과

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>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);

 

 

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

Clock이 처음 DOM에 렌더링 될 때마다 타이머를 설정하려고 한다. 이것은 React에서 마운팅이라고 한다.

또한 Clock에 의해 생성된 DOM이 삭제될 때마다 타이머를 해제하려고 한다. 이것을 언마운팅이라고 한다.

 

컴포넌트 클래스에서 특별한 메서드를 선언하여 컴포넌트가 마운트되거나 언마운트 될 때 일부 코드를 작동할 수 있다.

이러한 메서드들은 생명주기 메서드라고 불린다.

 

componentDidMount() 메서드는 컴포넌트 출력물이 DOM에 렌더링 된 후에 실행된다.이 장소가 타이머를 설정하기에 좋은 장소이다.

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

this.props가 React에 의해 스스로 설정되고 this.state가 특수한 의미가 있지만, 타이머 ID와 같이 데이터 흐름 안에 포함되지 않는 어떤 항목을 보관할 필요가 있다면 자유롭게 클래스에 수동으로 부가적인 필드를 추가해도 된다.

 

componentWillUnmount() 메서드는 DOM이 삭제될 때 실행된다.

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

 

 

전체적인 흐름

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>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
  1. <Clock />가 root.render()로 전달되었을 때 React는 Clock 컴포넌트의 constructor를 호출한다.
    Clock이 현재 시각을 표시해야 하기 때문에 현재 시각이 포함된 객체로 this.state를 초기화한다. 나중에 이 state를 업데이트할 것이다.
  2. React는 Clock 컴포넌트의 render() 메서드를 호출한다. 이를 통해 React는 화면에 표시되어야 할 내용을 알게 된다. 그 다음 React는 Clock의 렌더링 출력값을 일치시키기 위해 DOM을 업데이트한다.
  3. Clock 출력값이 DOM에 삽입되면, React는 componentDidMount() 생명주기 메서드를 호출한다.
    그 안에서 Clock 컴포넌트는 매초 컴포넌트의 tick() 메서드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청한다.
  4. 매초 브라우저가 tick() 메서드를 호출한다. 그 안에서 Clock 컴포넌트는 setState()에 현재 시각을 포함하는 객체를 호출하면서 UI 업데이트를 진행한다.
    setState() 호출 덕분에 React는 state가 변경된 것을 인지하고 화면에 표시될 내용을 알아내기 위해 render() 메서드를 다시 호출한다. 이 때 render() 메서드 안의 this.state.date가 달라지고 렌더링 출력값은 업데이트된 시각을 포함한다. React는 이에 따라 DOM을 업데이트한다.
  5. Clock 컴포넌트가 DOM으로부터 한 번이라도 삭제된 적이 있다면 React는 타이머를 멈추기 위해 componentWillUnmount() 생명주기 메서드를 호출다.

 

 

State 올바르게 사용하기

1. 직접 State 수정하지 않기

// Wrong
this.state.comment = 'Hello';

// Correct
this.setState({comment: 'Hello'});

this.state를 지정할 수 있는 유일한 공간은 constructor이다.

 

 

2. State 업데이트는 비동기적일 수도 있다.

React는 성능을 위해 여러 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다.

this.props와 this.state가 비동기적으로 업데이트될 수 있기 떄문에 다음 state를 계산할 때 해당 값에 의존해서는 안된다.

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

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

객체보다는 함수를 인자로 사용하는 형태의 setState를 사용하면 이전 state를 첫 번째 인자로 받아들일 것이고, 업데이트가 적용된 시점의 props를 두 번째 인자로 받아들일 것이다. 화살표 함수가 아닌 일반 함수에서도 정상적으로 작동한다.

 

 

3. State 업데이트는 병합된다.

setState()를 호출할 때 React는 제공한 객체를 현재 state로 병합한다.

예를 들어, state는 다양한 독립적인 변수를 포함할 수 있다.

  constructor(props) {
    super(props);
    this.state = {
      posts: [],
      comments: []
    };
  }

 

별도의 setState() 호출로 이러한 변수를 독립적으로 업데이트할 수 있다.

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

 

 

데이터는 아래로 흐른다.

State는 종종 로컬 또는 캡슐화라고 불린다. state가 소유하고 설정한 컴포넌트 이외에는 어떠한 컴포넌트에도 접근할 수 없다.

 

컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있다.

<FormattedDate date={this.state.date} />

FormattedDate 컴포넌트는 date를 자신의 props로 받을 것이고 이것이 Clock의 state로부터 왔는지, Clock의 props에서 왔는지, 수동으로 입력한 것인지 알지 못한다.

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

 

일반적으로 이를 하향식 또는 단방향식 데이터 흐름이라고 한다.

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

 

 

function App() {
  return (
    <div>
      <Clock />
      <Clock />
      <Clock />
    </div>
  );
}

각 Clock은 자신만의 타이머를 설정하고 독립적으로 업데이트를 한다.

유상태 컴포넌트 안에서 무상태 컴포넌트를 사용할 수 있으며, 그 반대 경우도 마찬가지로 사용할 수 있다.