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

리액트 실습 2 - Monsters Rolodex

by soldonii 2019. 11. 29.

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

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


1. Search Box 컴포넌트

searchBox 컴포넌트는 class 대신 함수를 이용해서 컴포넌트를 생성할 것이다. class를 이용하는 것과 함수를 이용하는 것의 근본적인 차이는 다음과 같다.

1) 컴포넌트 내부에서 state를 생성 및 조작하거나, 2) React Component 클래스에 내장된 life-cycle 메소드를 사용해야 할 경우에는 class를 이용해야 하고, 그렇지 않을 경우에는 함수를 이용할 수 있다.

단지 state를 props로 전달받아서, HTML을 리턴시키는 등의 작업에는 굳이 class를 이용해서 컴포넌트를 만들지 않아도 된다. 

 

// SearchBox 컴포넌트
const SearchBox = ({ placeholder, handleChange }) => (
  <input 
    className="search"
    type="search"
    placeholder={ placeholder }
    onChange={ handleChange }
  />
)

우선 destructuring을 활용해서 변수 매개변수 placeholder와 handleChange에 props로 들어올 객체에 담겨 있는 값을 할당했다.

(아래에 보면 있지만, App.js 파일에서 SearchBox 컴포넌트의 프로퍼티로 placeholder = 'search monsters', handleChange = {e => this.setState({searchField: e.target.value})} 를 설정했다. 그렇기 때문에 SearchBox 컴포넌트 내에서 props는 {placeholder: 'search monsters', handleChange: 함수} 이렇게 들어오기 때문에 {}로 같은 변수명을 destructuring 해주면 매개변수 placeholder에는 'search monsters'가, handleChange에는 함수가 들어오게 된다.)

 

이후 input 태그를 리턴시키는데, css 속성을 주기 위해 className을 세팅하고, type은 search, placeholder에는 비구조화로 매개변수 placeholder에 담겨있는 값인 'search monsters'를, onChange에는 handleChange에 담겨있는 함수를 전달한 형태이다.

굳이 이렇게 다소 복잡해보이는 컴포넌트를 만드는 이유는, 이렇게 함으로써 이 SearchBox 컴포넌트는 어디에서나 재사용이 가능하기 때문이다.

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

    return (
      <div className='App'>
        <SearchBox 
          placeholder= 'search monsters'
          handleChange= {e => this.setState({searchField: e.target.value})}
        />
        <CardList monsters={filteredMonsters}></CardList>
      </div>
    );
  }

 

2. Class Method and Arrow Function

Class 내부에서 함수를 선언할 때의 주의사항을 살펴보자.

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

    return (
      <div className='App'>
        <SearchBox 
          placeholder= 'search monsters'
          handleChange= {e => this.setState({searchField: e.target.value})}
        />
        <CardList monsters={filteredMonsters}></CardList>
      </div>
    );
  }

지금 보면 render() 메소드 내부에서 handleChange 프로퍼티의 값으로 함수를 선언했는데 이 코드 또한 재사용가능하게 하기 위해서 따로 독립된 함수로 빼내보자.

 

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

    this.state = {
      monsters: [],
      searchField: ''
    }
  }

  handleChange(e) { // 1. handleChange 메소드를 App class 내부에 선언했다.
    this.setState({searchField: e.target.value});
  }

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

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

    return (
      <div className='App'>
        <SearchBox 
          placeholder= 'search monsters'
          handleChange= {this.handleChange} // 2. handleChange 메소드를 불러온다.
        />
        <CardList monsters={filteredMonsters}></CardList>
      </div>
    );
  }
}

위 코드처럼 render() 메소드 밖으로 handleChange 메소드를 빼서 따로 만든 후에, handleChange 프로퍼티의 값으로 this.handleChange를 주었다. 하지만 이 코드는 실행이 되지 않는다.

 

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

여기에서 this의 컨텍스트는 명시되어 있지 않은 상태이다. 따라서 원하는 대로 실행하기 위해서는 this를 명시적으로 지정해주어야 한다. 이를 위해서는 App class의 코드들이 실행되기 이전에 가장 먼저 실행되는 constructor() 함수 내부에서 handleChange 메소드 내부의 context를 지정해 주어야 하는 것이다.

 

constructor() {
    super();
    this.state = {
      monsters: [],
      searchField: ''
    }
    this.handleChange = this.handleChange.bind(this); // App class의 context를 binding
  }

따라서 constructor 내부에서 맨 아래 코드처럼 this.handleChange = this.handleChange.bind(this) 이렇게 this.handleChange 메소드의 컨텍스트를 App 클래스로 바인딩 시켜주어야 한다. 그런데 매번 이렇게 코드를 추가로 입력하는 것은 번거롭다. 대안이 없을까?

 

this는 원래 dynamically scoped된 특성을 지닌다. 즉, 어떤 방식으로 호출하느냐에 따라서 this의 값이 변경되는 것이다. 그런데 이 특성을 lexically scoped 되게 변경시키는 방법을 예전에 이 글에서 배운 적이 있다. 바로 화살표 함수를 사용하면, this를 포함한 함수가 선언된 context가 바로 this가 된다. 

 

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

    this.state = {
      monsters: [],
      searchField: ''
    }
  }

  handleChange = (e) => { // arrow function으로 변경시키면, 이제 this는 이 context가 된다.
    this.setState({searchField: e.target.value});
  }

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

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

    return (
      <div className='App'>
        <SearchBox 
          placeholder= 'search monsters'
          handleChange= {this.handleChange}
        />
        <CardList monsters={filteredMonsters}></CardList>
      </div>
    );
  }
}

따라서 이처럼 handleChange 메소드를 화살표 함수로 선언하면, 이 경우 this의 context는 바로 이 App class가 된다.(헷갈려서 잘 이해가 안된다...ㅠㅠ)

 

import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
  constructor() {
    super();
    this.handleClick2 = this.handleClick1.bind(this);
  }

  handleClick1() {
    console.log(this, 'button 1 clicked');
  }

  handleClick3 = () => console.log(this, 'button 3 clicked');

  render() {
    return (
      <div>
        <button onClick={this.handleClick1()}>click 1-1</button>
        <button onClick={this.handleClick1}>click 1-2</button>
        <button onClick={this.handleClick2}>click 2</button>
        <button onClick={this.handleClick3}>click 2</button>
      </div>
    );
  }
}

export default App;

위 코드를 실행해보면서 좀 전까지 설명한 this를 조금 더 이해해보자.

- 첫번째 버튼, click 1-1은 버튼을 클릭을 해도 실행되지 않는다. 왜냐하면 onClick 메소드 내에서 이미 실행을 시켜버렸기 때문에, 파일 로딩과 동시에 click 1-1은 자동으로 실행되고 끝나버린다.

- 두번째 버튼, click 1-2는 버튼 클릭 시 'button 1 clicked'가 실행된다. 여기서 this는 무엇이 출력될까? 여기서 this는 undefined이다. 컨텍스트가 명시되어 있지 않기 때문이다.(?)

- 세번째 버튼, click 2는 버튼 클릭 시 'button 1 clicked'가 나오고 동시에 this 또한 App 객체가 된다. constructor 내부에서 binding 했기 때문이다.

- 네번째 버튼, click 3은 버튼 클릭 시 'button 2 clicked'가 출력되고, 또 마찬가지로 this 또한 App이 된다. 화살표 함수로 인해서 lexically scoped 되었기 때문이다.

 

React Event Handling Docs

댓글