All Articles

ReactJS 기본3

ReactJS 번역 및 정리한 내용. 언어 기능이 생겨 현재는 일부 한글로 번역되어 있는 상태이다. 사내 발표용으로 작성하였다.

  • 조건부 렌더링
  • 리스트와 Key

조건부 렌더링

JS에서의 조건 처리와 같은 방식으로 동작합니다. if 혹은 switch-case연산자와 같은 JS 연산자를 사용하여 조건과 일치하게 UI를 업데이트 합니다.

아래를 보시죠:

const UserGreeting = (props) => (
  return <h1>Welcome back!</h1>;
);

const GuestGreeting = (props) => (
  return <h1>Please sign up.</h1>;
);

로그인 했는지 여부에 따라 UserGreeting이나, GuestGreeting을 렌더링 하도록 하는 Greeting 컴퍼넌트를 만들어볼게요.

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

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

isLoggedIn props 값에 따라 다른 인사말을 렌더링합니다.

엘리먼트 변수

변수를 사용해서 elements를 저장할 수 있어요. 조건에 따라 Component의 일부를 변화된 부분만 렌더링 하도록 도와줄 겁니다.

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

const LogoutButton = (props) => (
    <button onClick={props.onClick}>
      Logout
    </button>
);

이제 LoginControl Component를 만들어 볼게요.

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

if 문을 사용해서 조건부 렌더링 하는것도 좋은 방법이지만, 더 짤은 구문을 사용하고 싶으면 어떻게 해야할까요? 아래에서 확인하시죠 : )

논리 && 연산자로 IF문을 인라인으로 표현하기

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

const 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')
);

이게 왜 실행될까요?

JS에서는 true && expression이면 항상 expression으로 평가되고, false && expression은 항상 false로 평가됩니다.

따라서 && 뒤의 엘리먼터는 조건이 true일때 렌더링 됩니다. false이면 React는 무시합니다.

3항연산자로 If-Else문을 인라인으로 표현하기

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

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

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

render() {
  const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      {isLoggedIn ? (
        <LogoutButton onClick={this.handleLogoutClick} />
      ) : (
        <LoginButton onClick={this.handleLoginClick} />
      )}
    </div>
  );
}

조건이 너무 복잡하다면, 컴퍼넌트를 분리하기 좋을 때 일수도 있다는 것을 기억하세요.

컴퍼넌트 렌더링 막기

가끔 다른 컴포넌트에 의해 렌더링될 때 컴포넌트 자체를 숨기고 싶을 때가 있을 수 있습니다. rendering 할 때 null을 리턴하면 됩니다.

function WarningBanner(props) {
  // props에 warn이 없으면 null 리턴
  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(prevState => ({
      showWarning: !prevState.showWarning
    }));
  }

  render() {
    return (
      <div>
        <!-- WarningBanner에 warn 속성을 전달 -->
        <WarningBanner warn={this.state.showWarning} />
        <button onClick={this.handleToggleClick}>
          {this.state.showWarning ? 'Hide' : 'Show'}
        </button>
      </div>
    );
  }
}

리스트와 Key

JS에서 map 함수

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

콘솔에 [2,4,6,8,10]을 출력합니다.

React에서 배열은 이와 거의 동일합니다.

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

ReactDOM.render(
  <ul>{listItems}</ul>,
  document.getElementById('root')
);

기본 리스트 컴퍼넌트

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

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

const 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];
ReactDOM.render(
  <NumberList numbers={numbers} />,
  document.getElementById('root')
);

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

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 이렇게 key를 주고 refactoring을 해볼게요.
    <li key={number.toString()}>
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

Key

Element에 id를 제공하기 위해 키를 지정합시다. Key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕습니다. key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야 합니다.

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

key를 선택할 때는 고유하게 실별하는 문자열을 사용하는 것이 좋습니다.

대부분의 경우 데이터의 ID를 키로 사용합니다.

const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

ID도 없다… index를 씁시다.

const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs
  <li key={index}>
    {todo.text}
  </li>
);

그런데.. List의 순서가 변경될 수 있는경우 키에 index를 사용하는 것은 안좋습니다. 이 때문에 성능이 저하되고, Component state에 문제가 발생할 수 있습니다.

Key로 컴퍼넌트 추출하기

key는 배열 주변의 Context에서만 의미가 있습니다.

잘못된 키 사용

const ListItem = (props) => {
  const value = props.value;
  return (
    // 틀렸어요! 여기에는 key를 지정할 필요가 없습니다.
    <li key={value.toString()}>
      {value}
    </li>
  );
}

const NumberList = (props) => {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 틀렸어요! 여기에 key를 지정해야 합니다.
    <ListItem value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

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

올바른 키 사용

const ListItem = (props) => {
  // 맞아요! 여기에는 key를 지정할 필요가 없습니다.
  return <li>{props.value}</li>;
}

const NumberList = (props) => {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 맞아요! 배열 안에 key를 지정해야 합니다.
    <ListItem key={number.toString()}
              value={number} />
  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

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

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

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

const Blog = (props) => {
  const sidebar = (
    <ul>
      {props.posts.map((post) =>
        // 여기에 post.id
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );
  const content = props.posts.map((post) =>
    // 여기에 post.id 같아도 됩니다. 별도의 배열이니까요.
    <div key={post.id}>
      <h3>{post.title}</h3>
      <p>{post.content}</p>
    </div>
  );
  return (
    <div>
      {sidebar}
      <hr />
      {content}
    </div>
  );
}

const posts = [
  {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
  {id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
  <Blog posts={posts} />,
  document.getElementById('root')
);

키는 React에 힌트 역할을 제공 하지만 Component에 전달되지는 않아요. 한번 더 전달 해주면 됩니다.

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

JSX에 map() 포함시키기

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

  );
  return (
    <ul>
      {listItems}
    </ul>
  );
}

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

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

      )}
    </ul>
  );
}

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


<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>

웹에서 다음과 같은 Form이 존재할 경우 name이라는 필드의 값이 전달됩니다.

React에서도 똑같이 동작합니다.

그런데 서버로 전송하기 전에 입력된 폼의 밸리데이션을 체크한다거나, 사용자의 입력을 어플리케이션에서 저장하고 싶으면 어떻게 해야 할까요?

JS에서 더 쉽게 접근할 수 있는 방법은 없을까요?

제어 컴퍼넌트

HTML form Element들 중에서 input, textarea, select등이 사용자와 상호작용 할 수 있는 녀석들입니다. 그리고 state는 mutable 합니다.

이 두 가지를 활용해서 컨트롤 가능한 Component를 만들어 볼게요.

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

input에 onChange에 handleChange를 바인딩 했습니다. input의 값이 변경되면 handleChange 가 호출 될 거에요.

그리고 form 의 onSubmithandleSubmit 을 바인딩 했습니다. submit 버튼이 클릭해서 submit 이벤트가 발생하면 handleSubmit이 호출 될 거에요.

textarea 태그

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

<textarea>
  Hello there, this is some text in a text area
</textarea>

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

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>는 드롭 다운 목록을 만듭니다. 예를 들어, 이 HTML은 과일 드롭 다운 목록을 만듭니다.

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

selected 옵션이 있으므로 Coconut 옵션이 초기값이 되는 점을 주의해주세요. 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>
    );
  }
}

복수 선택 가능하게 하기

<select multiple={true} value={['B', 'C']}>

file input 태그

<input type="file" />

이 값은 읽기 전용이므로 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 Null 값

input value에 prop을 할당 해놓으면 수정이 안되는데, 이를 null로 바꿔주면 사용자 입력이 가능하게 된다.

이를 응용해서 처음에는 입력이 안되지만, 잠시 후 수정이 가능하게 되도록 해보자.

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

setTimeout(function() {
  ReactDOM.render(<input value={null} />, mountNode);
}, 1000);

제어 컴퍼넌트의 대안

데이터를 변경할 수있는 모든 방법에 대해 이벤트 핸들러를 작성하고 React 구성 요소를 통해 모든 입력 상태를 연결해야하기 때문에 제어 된 구성 요소를 사용하는 것이 때로는 지루할 수 있습니다. 기존의 코드베이스를 React로 변환하거나 React 애플리케이션을 React 이외의 라이브러리와 통합 할 때 이것은 특히 성가신 일이 될 수 있습니다.

이럴 때는 아래 링크를 한번 봐주세요.

비제어 Component들