본문 바로가기
  • soldonii's devlog
Javascript 공부/React

리액트 실습 1 - Monsters Rolodex

by soldonii 2019. 11. 29.

*Udemy의"Complete React Developer in 2020"강의에서 학습한 내용을 정리한 포스팅입니다.

*자바스크립트와 리액트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. Card-list 컴포넌트 만들기

// card-list.component.jsx 파일
import React from 'react';

export const CardList = (props) => {
  console.log(props);
  return <div>Hello</div>;
}

새로운 컴포넌트 CardList를 만든 후, App.js에서 import할 수 있도록 export 시켰다.

 

import React, { Component } from 'react';
import { CardList } from './components/card-list/card-list.component'; // import했다.
import './App.css';

class App extends Component {
  constructor() {
    super();

    this.state = {
      monsters: []
    }
  }

  componentDidMount() {
    fetch('http://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(users => this.setState({ monsters: users }));
  }

  render() {
    return (
      <div className='App'>
        <CardList name="hyunsol" age="29"/> // CardList 컴포넌트에 property를 부여했다.
        {this.state.monsters.map(monster => (
          <h1 key={monster.id}>{ monster.name }</h1>
        ))}
      </div>
    );
  }
}

export default App;

CardList 컴포넌트는 매개변수로 props를 받은 후, console.log 하도록 되어 있는데 여기서 props에는 뭐가 들어가게 될까? CardList 컴포넌트를 import하는 곳에서 CardList 컴포넌트에 부여한 property가 바로 props에 객체의 형태로 들어가게 된다. 즉, props는 {name: "hyunsol", age: "29"} 객체가 들어가게 되는 것이다. (반드시 이름이 props일 필요는 없다.)

 

또 props 객체는 여러 프로퍼티를 가지고 있는데, children도 그 중 하나이다. props.children은 컴포넌트를 import한 곳에서, 컴포넌트 사이에 들어오는 내용이 들어가게 된다.

 

// App.js 파일
render() {
  return (
    <div className='App'>
      <CardList name="hyunsol" age="29">
        <h1>My Name is Hyunsol</h1> // 여기에 입력하는 내용이 children
      </CardList>
      {this.state.monsters.map(monster => (
        <h1 key={monster.id}>{ monster.name }</h1>
      ))}
    </div>
  );
}

// card-list.component.jsx 파일
import React from 'react';

export const CardList = (props) => {
  return <div>{ props.children }</div> // prop.children을 리턴.
}

위 코드를 보면, CardList 컴포넌트 사이에 <h1> element가 들어가 있다.  이 경우 card-list.component.jsx 파일에서 props.children은 곧 <h1>My Name is Hyunsol</h1>을 의미한다. 

 

핵심 1 : 컴포넌트의 매개변수(ex. props)에는 컴포넌트의 프로퍼티가 객체의 형태로 담긴다.
핵심 2 : 매개변수의 children은 컴포넌트 사이에 입력된 코드이다.

 

2. CardList 컴포넌트 refactoring하기

CardList 컴포넌트가 카드들을 화면에 보여주는 레이아웃의 역할을 한다면, Card 자체에 해당되는 컴포넌트를 생성해 볼 차례이다. 그런데 그 이전에 수정할 내용이 있다. 현재까지 완성된 내용을 살펴보자.

// App.js
render() {
  return (
    <div className='App'>
      <CardList className='card-list'>
        {this.state.monsters.map(monster => (
          <h1 key={monster.id}>{ monster.name }</h1>
        ))}
      </CardList>
    </div>
  );
}

// card-list.component.jsx 파일
const CardList = props => (
  <div className="card-list">{props.children}</div>
)

현재 App.js 파일에서 state에 존재하는 monsters 배열의 값을 조작한 후, CardList 컴포넌트에게 전달하여 h1 element를 표시하도록 하고 있다. 그런데 엄밀히 따지면, 이 역할은 CardList 컴포넌트의 역할이다. 따라서 이를 수정해보면 아래와 같다.

 

// App.js
render() {
  return (
    <div className='App'>
      <CardList monsters={this.state.monsters}></CardList>
    </div>
  );
}

// card-list.component.jsx 파일
const CardList = props => (
  <div className="card-list">
    {props.monsters.map(monster => (
      <h1 key={monster.id}>{ monster.name }</h1>
    ))}
  </div>
)

App.js에서는 단지 state의 monsters 배열을 CardList 컴포넌트의 프로퍼티로 전달만 해준다. 그리고 전달받은 데이터를 기반으로 데이터를 조작해서 h1 element로 화면에 그려지게 하는 역할을 CardList 컴포넌트에게 위임하였다.

 

3. State vs. Props

class App extends Component {
  constructor() {
    super();

    this.state = {
      monsters: []
    }
  }

  componentDidMount() {
    fetch('http://jsonplaceholder.typicode.com/users') // api로 요청을 보내는 작업
    .then(response => response.json()) // 전달받은 응답을 자바스크립트가 이해할 수 있도록 json 형태로 변환
    .then(users => this.setState({ monsters: users })); // state의 monster 데이터를 응답받은 user 데이터로 바꿔주기
  }

  render() {
    return (
      <div className='App'>
        <CardList monsters={this.state.monsters}></CardList>
      </div>
    );
  }
}

현재 시점에서 state는 App.js 파일 내에서만 존재한다. 그리고 trickle down 하면서, 즉 one-way data flow에 의거하여 아래 쪽의 컴포넌트들에게 현재 state의 데이터가 전달된다. 그러면 이 전달된 데이터가 해당 컴포넌트 안에서는 props가 되는 것이다.

결론적으로, 특정 위치에 존재하는 state는 컴포넌트에 전달되면서 컴포넌트 내에서 props가 된다.

 

4. SearchField State

이제 로봇을 검색하면 해당 문자열을 포함한 로봇만을 출력시키는 SearchField 컴포넌트를 만들어보자.

class App extends Component {
  constructor() {
    super();

    this.state = {
      monsters: [],
      searchField: '' // 1. 입력한 데이터를 저장할 searchField를 만들어준다.
    }
  }

  componentDidMount() {
    fetch('http://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(users => this.setState({ monsters: users }));
  }

  render() {
    return (
      <div className='App'>
        <input type="search" placeholder="search monsters" // 2. input HTML 태그를 생성한다.
          onChange={e => {
            this.setState({searchField: e.target.value});
            console.log(this.state.searchField);
          }}/>
        <CardList monsters={this.state.monsters}></CardList>
      </div>
    );
  }
}

우선 App.js 내에 정보를 입력받을 수 있는 input tag를 생성한 후, property를 부여해준다. 이 때 핵심이 되는 프로퍼티는 onChange 메소드이다.

 

# onChange 메소드

onChange 메소든느 input에 변화가 생길 때마다 실행되는 event 메소드이다. 위 코드에서는 이벤트가 발생할 때마다, setState 메소드를 이용하여 state 내의 searchField 값을 e.target.value, 즉 input 태그에 입력된 이벤트 값이 설정되도록 했다. 이제 글자가 입력/삭제될 때마다 state의 searchfield 값이 변경이 되는데, 딱 하나 문제점이 있다.

 

현재 상태로 코드를 실행해보면, 사용자가 입력한 input보다 한 템포가 느리게 searchfield 값이 변경된다. 즉, 이용자가 처음 's'를 입력했을 때 searchfield는 '' 비어있는 상태이고, 'sa'를 입력하면 그제서야 's'가 들어오게 된다.

 

이 문제의 원인은 setState 메소드의 작동 방식 때문이다. setState 메소드가 비동기적으로 작동하기 때문이다. 이를 해결하기 위해서는 setState 메소드에 두번째 argument로 콜백 함수를 전달해야 한다.

 

// 기존 code
<input type="search" placeholder="search monsters"
          onChange={e => {
            this.setState({searchField: e.target.value});
            console.log(this.state.searchField);
          }}/>

// solution code
<input type="search" placeholder="search monsters" 
          onChange={e => {
            this.setState({searchField: e.target.value}, () => // 두번째 인자로 콜백 함수를 넘겨줌.
                          console.log(this.state.searchField));
          }}/>

setState 메소드의 두번째 인자로 콜백 함수를 전달하였다. 이 콜백함수는 setState 메소드의 실행이 끝나면, 즉 코드에서 지시한대로 state의 값을 변경하는 작업이 종료되면 실행되는 함수이다.

 

이를 조금 더 자세하게 설명해보자.

React는 매우 똑똑한 라이브러리이고, DOM 조작에 대해서 개발자가 신경쓰지 않도록, React가 스스로 판단해여 DOM을 변화시킨다고 했다. 즉, state의 변화를 감지해서 DOM을 알아서 re-rendering한다. 콜백함수로 넘겨주기 전에 한 박자씩 state 변화가 늦었던 이유는, react가 바로 DOM을 re-rendering하지 않고, 언제 re-rendring할지 판단했기 때문이다. 

예를 들어, 위 코드 뿐 아니라 만약 다른 여러 장소에서 state가 변경될 경우에는 그러한 변경까지 모두 포함해서 DOM re-rendering을 한 번만 수행하는 것이 성능 측면에서 효율적이기 때문에 react가 이를 판단하느라 동시다발적으로 변화가 일어나지 않았던 것이다. 이 때 setState의 두번째 인자로 콜백함수를 전달하면, 리액트가 DOM re-rendering을 최적화하는 대신, setState 작업이 완료되면 바로 re-rendering하도록 명령한 행위이다.

 

핵심 : setState로 state 변경 후 바로 이어서 작업을 해야할 경우, 두번째 인자로 콜백함수를 넘겨준다.

 

5. React Event

onChange 메소드는 리액트 라이브러리에 내장된 메소드이다. 이 메소드는 DOM에서 user event가 발생하면, 리액트 라이브러리에게 변화가 발생했음을 알려주고, onChange 메소드 내부에 정의된 일을 수행하는 역할을 한다. 그리고 작업을 마치면 ReactDOM 라이브러리는 기존 DOM과 onChange 작업 후 변화된 DOM을 비교해서 re-rendering한다.

 

즉, onChange 메소드는 작업 후 render를 유발하는 메소드이다. 그런데 현재 우리 코드는 render() 메소드 내부에서 onChange 메소드를 사용하고 있기 때문에, 이러한 경우 rendering 과정에서 loop이 형성되어서 오류가 발생할 수 있다(고 한다.) 물론 현재 코드는 onChange 메소드가 단지 함수를 정의할 뿐이고 실행하진 않기 때문에 괜찮으나, 명심해야 할 점은 onChange 메소드는 언제나 render() 메소드 내부가 아닌 외부에서 이뤄져야 한다는 점이다.

 

6. Filtering State

user가 입력한 글자를 포함한 monster만 화면에 표시하기 위한 작업을 해보자. 다소 복잡할 수 있으므로 천천히 이해해보자.

render() {
    const { monsters, searchField } = this.state;
    const filteredMonsters = monsters.filter(monster => monster.name.toLowerCase().includes(searchField.toLowerCase()))

    return (
      <div className='App'>
        <input type="search" placeholder="search monsters" 
          onChange={e => {
            this.setState({searchField: e.target.value}, () => console.log(this.state.searchField));
          }}/>
        <CardList monsters={filteredMonsters}></CardList>
      </div>
    );
  }

1. render() 메소드 내부에 새로운 filteredMonsters 배열을 만든다. 여기에는 state의 monsters 배열에서 이용자가 입력한 값을 포함하고 있는 monster만을 추려낸 배열이다.

2. 이 배열을 CardList 컴포넌트에게 넘겨준다.

// CardList 컴포넌트
const CardList = props => (
  <div className="card-list">
    {props.monsters.map(monster => (
      <Card key={monster.id} monster={monster} />
    ))}
  </div>
)

3. CardList 컴포넌트 내부에서 props는 {monsters: filteredMonsters}가 된다. searchfield state의 문자열을 포함하고 있는 monster만 담겨있는 배열이다. 이 배열을 map으로 돌면서 개별 monster element를 Card 컴포넌트에게 넘겨준다.

 

// Card 컴포넌트
const Card = (props) => (
  <div className='card-container'>
    <img alt="monster" src={`https://robohash.org/${props.monster.id}?set=set2&size=180x180`}/>
    <h2> { props.monster.name }</h2>
    <p> {props.monster.email } </p>
  </div>
)

4. Card 컴포넌트 내부에서 props는 map으로 돌면서 전달받은 각각의 개별 monster이다. 이를 전달받은 후, 해당 monster의 id, name, email을 div에 담아서 리턴시킨다.

 

- 만약 user가 다른 값을 추가로 입력하거나 삭제하면 onChange 메소드가 발동되고, onChange 메소드 내부에서 setState를 통해 App.js 내에 존재하는 state의 값을 변경시킨다.

- state가 변경되면 ReactDOM 라이브러리는 변화를 감지해서 DOM을 다시 rendering한다.

- render() 메소드 내부에 filteredMonsters가 정의되어 있기 때문에, 현재 변경된 state의 searchfield 값을 기반으로, 해당 문자열을 포함하고 있는 filteredMonsters 배열을 다시 만들어낸다. 그 이후부터는 위 과정의 반복이다.

댓글