개발 기록 남기기✍️

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

Front-End/React

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

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

6. 이벤트 처리하기

React 엘리먼트와 DOM 엘리먼트의 이벤트 처리 방식 문법 차이

  • React의 이벤트는 소문자 대신 카멜 케이스를 사용한다.
  • JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달한다.
  • React는 기본 동작을 방지하려면 반드시 preventDefault를 명시적으로 호출해야 한다.
/* HTML */
<button onclick="activateLasers()">
  Activate Lasers
</button>


/* React */
<button onClick={activateLasers}>
  Activate Lasers
</button>
/* React preventDefault */
function Form() {
  function handleSubmit(e) {
    e.preventDefault();
    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

React 이벤트는 브라우저 고유 이벤트와 정확히 동일하게 동작하지는 않는다.

React를 사용할 때 리스너를 추가하기 위해 addEventListener를 호출할 필요가 없다. 대신, 엘리먼트가 처음 렌더링될 때 리스너를 제공하면 된다.

 

ES6 클래스를 사용하여 컴포넌트를 정의할 때, 일반적인 패턴은 이벤트 핸들러를 클래스의 메서드로 만드는 것이다.

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

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

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

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

클래스 메서드는 기본적으로 바인딩되어 있지 않다.

this.handleClick을 바인딩하지 않고 onClick에 전달했다면, 함수가 실제 호출될 때 this는 undefined가 된다.

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

 

bind를 호출하는 것이 불편하다면, 콜백을 올바르게 바인딩하기 위해 퍼블릭 클래스 필드 문법을 활용할 수 있다.

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에서는 이 문법이 기본적으로 설정되어 있다.

 

클래스 필드 문법을 사용하고 있지 않다면, 콜백에 화살표 함수를 사용하는 방법도 있다.

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

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

이 문법의 문제점은 LoggingButton이 렌더링될 때마다 다른 콜백이 생성된다는 것이다. 대부분의 경우 문제가 되지 않으나, 콜백이 하위 컴포넌트에 props로서 전달된다면 그 컴포넌트들은 추가로 다시 렌더링을 수행할 수도 있다.

이러한 종류의 성능 문제를 피하고자, 생성자 안에서 바인딩하거나 클래스 필드 문법을 사용하는 것을 권장한다.

 

 

이벤트 핸들러에 인자 전달하기

루프 내부에서는 이벤트 핸들러에 추가적인 매개변수를 전달하는 것이 일반적이다.

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

위 두 줄은 동등하며 각각 화살표 함수와 Function.prototype.bind를 사용한다.

 

두 경우 모두 React 이벤트를 나타내는 e 인자가 ID 뒤에 두 번째 인자로 전달된다. 화살표 함수를 사용하면 명시적으로 인자를 전달해야 하지만 bind를 사용할 경우 추가 인자가 자동으로 전달된다.

 


7. 조건부 렌더링

React에서 조건부 렌더링은 JavaScript에서의 조건 처리와 같이 동작한다. if나 조건부 연산자와 같은 JavaScript 연산자를 현재 상태를 나타내는 엘리먼트를 만드는데 사용한다. 그러면 React 현재 상태에 맞게 UI를 업데이트 한다.

function UserGreeting(props) {
  return <h1>Welcome back!</h1>;
}

function GuestGreeting(props) {
  return <h1>Please sign up.</h1>;
}

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

const root = ReactDOM.createRoot(document.getElementById('root')); 
// Try changing to isLoggedIn={true}:
root.render(<Greeting isLoggedIn={false} />);

이 예시는 isLoggedIn prop에 따라서 다른 인사말을 렌더링 한다.

 

 

엘리먼트 변수

엘리먼트를 저장하기 위해 변수를 사용할 수 있다.

출력의 다른 부분은 변하지 안은 채로 컴포넌트의 일부를 조건부로 렌더링할 수 있다.

function LoginButton(props) {
  return (
    <button onClick={props.onClick}>
      Login
    </button>
  );
}

function LogoutButton(props) {
  return (
    <button onClick={props.onClick}>
      Logout
    </button>
  );
}
class LoginControl extends React.Component {
  constructor(props) {
    super(props);
    this.handleLoginClick = this.handleLoginClick.bind(this);
    this.handleLogoutClick = this.handleLogoutClick.bind(this);
    this.state = {isLoggedIn: false};
  }

  handleLoginClick() {
    this.setState({isLoggedIn: true});
  }

  handleLogoutClick() {
    this.setState({isLoggedIn: false});
  }

  render() {
    const isLoggedIn = this.state.isLoggedIn;
    let button;
    if (isLoggedIn) {
      button = <LogoutButton onClick={this.handleLogoutClick} />;
    } else {
      button = <LoginButton onClick={this.handleLoginClick} />;
    }

    return (
      <div>
        <Greeting isLoggedIn={isLoggedIn} />
        {button}
      </div>
    );
  }
}

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

LoginControl이라는 유상태 컴포넌트는 현재 상태에 맞게 <LoginButton />이나 <LogoutButton />을 렌더링한다. 또한 <Greeting />도 함께 렌더링한다.

 

 

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

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

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'];

const root = ReactDOM.createRoot(document.getElementById('root')); 
root.render(<Mailbox unreadMessages={messages} />);

JavaScript에서 true && 표현식은 항상 표현식으로 평가되고 false && 표현식은 항상 false로 평가된다.

따라서 && 뒤의 엘리먼트는 조건이 true일 때 출력이 된다. 조건이 false라면 React는 무시하고 건너뛴다.

 

falsy 표현식을 반환하면 여전히 && 뒤에 있는 표현식은 건너뛰지만 falsy 표현식이 반환되는 것을 주의해야 한다.

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

render 메서드에서 <div>0</div>가 반환된다.

 

 

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

엘리먼트를 조건부로 렌더링하는 다른 방법은 조건부 연산자인 condition ? true : false를 사용하는 것이다.

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

가독성은 좀 떨어지지만, 더 큰 표현식에도 이 구문을 사용할 수 있다.

조건이 너무 복잡하다면 컴포넌트를 분리하기 좋은 때 일 수도 있다.

 

 

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

다른 컴포넌트에 의해 렌더링될 때 컴포넌트 자체를 숨기고 싶다면 렌더링 결과를 출력하는 대신 null을 반환하면 해결할 수 있다.

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

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

class Page extends React.Component {
  constructor(props) {
    super(props);
    this.state = {showWarning: true};
    this.handleToggleClick = this.handleToggleClick.bind(this);
  }

  handleToggleClick() {
    this.setState(state => ({
      showWarning: !state.showWarning
    }));
  }

  render() {
    return (
      <div>
        <WarningBanner warn={this.state.showWarning} />
        <button onClick={this.handleToggleClick}>
          {this.state.showWarning ? 'Hide' : 'Show'}
        </button>
      </div>
    );
  }
}

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

<WarningBanner />가 warn prop의 값에 의해서 렌더링된다. prop이 false라면 컴포넌트는 렌더링하지 않게 된다.

컴포넌트의 render 메서드로부터 null을 반환하는 것은 생명주기 메서드 호출에 영향을 주지 않는다. 그 예로 componentDidUpdate는 계속해서 호출되게 된다.

 


8. 리스트와 Key

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((number) => number * 2);
console.log(doubled);

이 코드는 콘솔에 [2, 4, 6, 8, 10]을 출력한다.

 

React에서 배열을 엘리먼트 리스트로 만드는 방식은 이와 거의 동일하다.

 

 

여러 개의 컴포넌트 렌더링 하기

엘리먼트 모음을 만들고 중괄호 {}을 이용하여 JSX에 포함 시킬 수 있다.

아래 JavaScript map() 함수를 사용하여 numbers 배열을 반복 실행한다. <li> 엘리먼트를 반환하고 엘리먼트 배열의 결과를 listItems에 저장한다.

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

그러면 <ul> 엘리먼트 안에 전체 listItems 배열을 포함할 수 있다.

<ul>{listItems}</ul>

 

 

기본 리스트 컴포넌트

일반적으로 컴포넌트 안에서 리스트를 렌더링한다.

 

이전 예시를 numbers 배열을 받아서 순서 없는 엘리먼트 리스트를 출력하는 컴포넌트로 리팩토링할 수 있다.

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

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

이 코드를 실행하면 리스트의 각 항목에 key를 넣어야 한다는 경고가 표시된다.

key는 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트이다.

 

 

Key

Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다.

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

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

대부분의 경우 데이터의 ID를 key로 사용한다. 이처럼 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것이 좋다.

렌더링 한 항목에 대한 안정적인 ID가 없다면 최후의 수단으로 항목의 인덱스를 key로 사용할 수 있다.

 

항목의 순서가 바뀔 수 있는 경우 key에 인덱스를 사용하는 것은 권장하지 않는다. 리스트 항목에 명시적으로 key를 지정하지 않으면 React는 기본적으로 인덱스를 key로 사용한다.

 

 

Key로 컴포넌트 추출하기

키는 주변 배열의 context에서만 의미가 있다.

예를 들어 ListItem 컴포넌트를 추출한 경우 ListItem 안에 있는 <li> 엘리먼트가 아니라 배열의 <ListItem /> 엘리먼트가 key를 가져야 한다.

 

map() 함수 내부에 있는 엘리먼트에 key를 넣어 주는 것이 좋다.

 

 

Key는 형제 사이에서만 고유한 값이어야 한다.

Key는 배열 안에서 형제 사이에 고유해야 하고 전체 범위에서 고유할 필요는 없다. 두 개의 다른 배열을 만들 때 동일한 key를 사용할 수 있다.

 

React에서 key는 힌트를 제공하지만 컴포넌트로 전달하지는 않는다. 컴포넌트에서 key와 동일한 값이 필요하다면 다른 이름의 prop으로 명시적으로 전달한다.

const content = posts.map((post) =>
  <Post
    key={post.id}
    id={post.id}
    title={post.title} />
);

위 예시에서 Post 컴포넌트는 props.id를 읽을 수 있지만 props.key는 읽을 수 없다.

 

 

JSX에 map() 포함시키기

JSX를 사용하면 중괄호 안에 모든 표현식을 포함시킬 수 있으므로 map() 함수의 결과를 인라인으로 처리할 수 있다.

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

이 방식을 사용하면 코드가 더 깔끔해 지지만, 이 방식을 남발하는 것은 좋지 않다.

JavaScript와 마찬가지로 가독성을 위해 변수로 추출해야 할지, 인라인으로 넣을 지는 개발자가 직접 판단해야 한다.

map() 함수가 너무 중첩된다면 컴포넌트로 추출하는 것이 좋다.

 


9. 폼

제어 컴포넌트

HTML에서 <input>, <textarea>, <select>와 같은 폼 엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트한다. React에서는 변경될 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트된다.

 

React state를 신뢰 가능한 단일 출처로 만들어 두 요소를 결합할 수 있다. 그러면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어한다. 이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 제어 컴포넌트라고 한다.

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

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

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

value 어트리뷰트는 폼 엘리먼트에 설정되므로 표시되는 값은 항상 this.state.value가 된고 React state는 신뢰 가능한 단일 출처가 된다.

 

제어 컴포넌트로 사용하면 input 값은 항상 React state에 의해 결정된다.다른 UI 엘리먼트에 input의 값을 전달하거나 다른 이벤트 핸들러에서 값을 재설정할 수 있다.

 

 

textarea 태그

HTML에서 <textarea> 엘리먼트는 텍스트를 자식으로 정의한다.

React에서 <textarea>는 value 어트리뷰트를 대신 사용한다. 이렇게 하면 <textarea>를 사용하는 폼은 한 줄 입력을 사용하는 폼과 비슷하게 작성할 수 있다.

class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 'Please write an essay about your favorite DOM element.'
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

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

  handleSubmit(event) {
    alert('An essay was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Essay:
          <textarea value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

this.state.value를 생성자에서 초기화하므로 textarea는 일부 텍스트를 가진 채 시작된다.

 

 

select 태그

HTML에서 <select>는 드롭 다운 목록을 만든다. React에서는 selected 어트리뷰트를 사용하는 대신 최상단 select 태그에 value 어트리뷰트를 사용한다. 한 곳에서 업데이트만 하면 되기 때문에 제어 컴포넌트에서 사용하기 더 편하다.

class FlavorForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: 'coconut'};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

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

  handleSubmit(event) {
    alert('Your favorite flavor is: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Pick your favorite flavor:
          <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>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

 

전반적으로 <input type="text">, <textarea> 및 <select> 모두 제어 컴포넌트를 구현하는데 value 어트리뷰트를 허용한다.

 

🚨주의🚨
select 태그에 multiple 옵션을 허용한다면 value 어트리뷰트에 배열을 전달할 수 있다.

 

 

file input 태그

값이 읽기 전용이기 때문에 React에서는 비제어 컴포넌트이다.

 

 

다중 입력 제어하기

여러 input 엘리먼트를 제어해야 할 때, 각 엘리먼트에 name 어트리뷰트를 추가하고 event.target.name 값을 통해 핸들러가 어떤 작업을 할 지 선택할 수 있게 해준다.

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 태그의 name에 일치하는 state를 업데이트하기 위해 ES6의 computed property name 구문을 사용하고 있다.

또한, setState()는 자동적으로 현재 state에 일부 state를 병합하기 때문에 바뀐 부분에 대해서만 호출하면 된다.

 

 

제어되는 Input Null 값

제어 컴포넌트에 value prop을 지정하면 의도되지 않는 한 사용자가 변경할 수 없다. value를 설정했는데 여전히 수정할 수 있다면 실수로 value를 undefined나 null로 설정했을 수 있다.

ReactDOM.createRoot(mountNode).render(<input value="hi" />);

setTimeout(function() {
  // input에 입력 가능해짐
  ReactDOM.createRoot(mountNode).render(<input value={null} />);
}, 1000);