props와 state는 다르다는것을 알고 상황에 알맞게 잘 사용을 해야한다. 두 객체는 모두 렌더링 결과물에 영향을 주는 정보를 갖고 있지만 두객체의 차이는 분명하다. props은 properties의 줄임말로 함수 매개변수처럼 컴포넌트에 전달되는 반면 state는 함수 내에 선언된 변수처럼 컴포넌트 안에서 관리가 된다.

프로젝트를 설계할때는 state가 최소화되도록 작성하는게 좋다. 중복으로 작성하면 UI의 상호작용에 혼란을 줄수 있다.
일단 state가 변경되면 컴포넌트는 렌더링을 다시 시도한다. 아래와 같은 앱을 만들때 어떤 값을 props으로 사용하고, 어떤 값을 state로 사용하는지 생각을 해보자

  • 부동산의 목록
  • 유저가 입력한 검색어
  • 체크박스의 값
  • 필터링 된 부동산 목록

데이터에 대해 아래의 세 가지 질문을 통해 결정이 가능하다.

  1. 부모로부터 props 을 통해 전달이 되는지?
  2. 시간이 지나도 변하지 않는지
  3. 컴포넌트 안의 다른 state나 props을 가지고 계산하는지

위 세가지에 해당되면 props 이다.
부동산의 목록은 props을 통해 전달되기 때문에 props이고, 검색어와 체크박스는 시간이 지남에 따라 변하기 때문에 state의 값으로 상태값을 업데이트 해야 한다. 필터링 된 제품의 목록은 컴포넌트 안의 검색어, 체크박스의 값으로 계산하기 때문에 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 />
      } />
  );
}

위처럼 작성하면 App에서 왼쪽은 Contacts, 오른쪽은 Chat의 컴포넌트가 화면에 렌더링 된다.

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

HTML 폼 element는 자체가 내부 상태를 가지기 때문에 React에서의 다른 element와는 조금 다르게 동작한다. 입력폼의 element를 만들고 입력받기 위해서는 아래와 같이 코드를 작성

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

html에서의 textarea는 자식에 값을 설정하지만 react에서 textarea는 attribute에 값을 설정

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

select의 경우는 아래와 같이 작성

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

checkbox를 사용할때는 아래와 같이 사용

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

key는 element의 리스트를 사용할때 꼭 명시해줘야 한다. 아래 소스코드를 참고하면 

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

위에서 li의 element에서 key를 사용하지 않으면 경고가 표시된다. 

React에서 Key의 역할은 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다. 그렇기 때문에 리스트 element에서 값의 업데이트를 위해서는 반드시 적어줘야 하는데, 이때 각각 element는 고유한 key를 갖고 있어야 하고, 보통은 데이터의 id를 사용한다. 

id가 없다면 최후의 수단으로 항목의 index를 키로 사용할 수 있는데, 이렇게 사용하면 성능이 저하되거나 컴포넌트의 state와 관련한 문제가 발생할 수 있으니 명시적으로 id를 적어주는게 좋다.

위에서 index를 지정하지 않으면 경고가 뜬다고 했는데, 그 이유도 기본으로 index를 key로 사용하기 때문에 표시되는 방법이다. (권장하지 않는다는 뜻!)

컴포넌트 추출하기

컴포넌트로 Blog를 추출하고 리스트의 값을 표시하기 위해서는 아래와 같이 코드를 작성하면 된다.

function ListItem(props) {  
  return <li>{props.value}</li>;
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) => 
    <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')
);
function Blog(props) {
  const sidebar = (
    <ul>
      {props.posts.map((post) =>
        <li key={post.id}>
          {post.title}
        </li>
      )}
    </ul>
  );
  const content = props.posts.map((post) =>
    <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')
);
 

javascript에서 map을 이용해 list의 값을 순차적으로 사용할 수 있음

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

JSX에서도 map을 이용해 아래와 같이 사용이 가능, element 모음을 만들고 중괄호를 이용해야 한다.

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

리스트의 값을 화면에 렌더링하기 위한 컴포넌트를 생성할때는 아래와 같이 사용이 가능하다. 이때 주의해야할 element list를 만들때는 element의 key가 반드시 필요하다. (이때 key는 고유성을 부여하기 위해서 고유하게 식별할 수 있는 문자열을 사용해야 한다. 보통 데이터의 ID를 key로 사용)

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

 

JSX 안에 && 의 연산자를 통해 mailbox의 읽지 않은 메시지의 길이가 0을 초과할때만 화면에 표시하도록 가능하다.c

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

참고로 javascript에서는  true && expression은 항상 expression으로 평가되고 false && expression은 항상 false로 평가 위에서 unreadMesages.length가 true라면 뒤의 조건을 출력하고, false라면 React에서는 무시한다.

조건부연산자인 condition ? true: false를 사용도 가능

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

아래도 가능! element를 조건부 연산자에서도 사용이 가능

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

로그인 사용자와 게스트 사용자를 조건을 통해서 element를 렌더링이 가능

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

login/logout 버튼 변경하기 상태에 따라 LogoutButton, LoginButton element를 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>
    );
  }
}

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

이렇게 분기하는게 좋을까? 분기를 해야하는 깊이가 커지면 모든 경우에 따라 처리를 해줘야 하겠구나. 두가지 조건이 있는 경우에는 두 조건의 조건부 렌더링을 해주는 컴포넌트(함수)를 만드는게 좋겠다.

+ Recent posts