본문 바로가기
프론트엔드 상식

React Event - React 컴포넌트 이벤트 만들기

by Whiimsy 2021. 1. 12.

컴포넌트에 이벤트를 추가하는 것은 애플리케이션에 역동성을 부여한다. 우리가 만들 이벤트는 특정 버튼을 눌러 원하는 페이지로 이동하거나 페이지를 이동하지 않고, 또는 페이지를 리로딩하지 않고 화면을 전환하는 이벤트이다.

 

🌭 State Setting

페이지의 state가 기본 모드인 welcome mode인지, 카테고리를 선택하면 밑에 설명이 나오는 read mode인지 구분하기 위해 constructor의 this.state에 mode를 추가한다. mode의 기본값으로는 'welcome'을 준다.

 

mode : 'welcome',

 

mode가 welcome일 때, contents에 표시할 문구도 this.state에 작성한다.

 

welcome : { title:'Welcome', desc:'Hello React!!' },

 

React의 경우 props나 state가 바뀔 때 마다 해당되는 component의 render 함수가 다시 호출된다. 여기서 render 함수는 어떤 HTML을 그릴 것인가를 결정하는 역할을 한다. mode가 바뀌면 render 함수의 결과가 바뀌도록 코드를 수정해보자. mode가 welcome일 경우 _title과 _desc를 위에 정의한 welcome state의 contents로 재정의하고, return 함수의 Content 컴포넌트의 title, desc 속성에 각각 _title, _desc를 불러온다.

 

render() {
    var _title, _desc;
    if (this.state.mode == "welcome") {
      _title = this.state.welcome.title;
      _desc = this.state.welcome.desc;
    } else if (this.state.mode == "read") {
    }

    return (
      <div className="App">
        <Subject
          title={this.state.subject.title}
          sub={this.state.subject.sub}
        ></Subject>
        <TOC data={this.state.contents}></TOC>
        <Content title={_title} desc={_desc}></Content>
      </div>
    );
  }

 

🌭 버튼 클릭했을 때 mode의 state 바뀌게 하기

 

Subject 컴포넌트의 title인 MEOW를 클릭했을 때, Subject 컴포넌트 밖의 페이지 영역인 Content 컴포넌트가 바뀌게 해야한다. 지금 App.js > Subject 컴포넌트를 주석 처리하고 Subject.js > return 함수 안의 내용을 원래 App.js > Subject 컴포넌트가 있던 자리에 붙여 넣는다. 그리고 props를 사용할 필요가 없기 때문에 state로 변경한다.

 

return (
      <div className="App">
        {/* <Subject
          title={this.state.subject.title}
          sub={this.state.subject.sub}
        ></Subject> */}
        <header>
        <h1>
          <a href="/">{this.state.subject.title}</a>
        </h1>
        {this.state.subject.sub}
      </header>
        <TOC data={this.state.contents}></TOC>
        <Content title={_title} desc={_desc}></Content>
      </div>
    );

 

우리가 하고자 하는 건, a 태그 링크를 클릭했을 때 Content 컴포넌트 영역에 Welcome 문구가 뜨게하는 특정 자바 스크립트 코드가 실행되게 하는 것이다. 우린 이 기능을 onClick 함수를 이용해 구현할 수 있다. a 태그의 링크를 클릭했을 때 onClick 안의 함수가 실행되도록 코드를 작성하자.

 

<a href="/" onClick={function(){ alert('hi'); }}>{this.state.subject.title}</a>

 

그런데 이 경우 알림창의 확인을 클릭했을 때, 페이지가 다시 리로드 된다. 이 문제를 해결하기 위해선 function의 파라미터로 e(event)를 주고 e.preventDefault(); 를 함수 안에 입력하면 된다. 이벤트가 발생한 태그의 기본적인 동작을 못하게 막는 역할을 하므로 이렇게 하면 페이지가 리로드 되지 않는다.

 

<a href="/" onClick={function (e) { e.preventDefault(); alert("hi"); }} > 
	{this.state.subject.title}
</a>

 

우리가 하고싶은건 a 태그를 클릭했을 때 App 컴포넌트의 mode 값을 welcome으로 바꾸는 것이다. constructor로 처음에 state를 정의하는 방식이 아닌 컴포넌트 내에서 state를 바꿀 땐 setState() 함수를 사용해야 한다.

 

<a href="/" onClick={function (e) { 
	e.preventDefault(); 
    this.setState({ mode: "welcome", }); 
}.bind(this)}>
    {this.state.subject.title}
 </a>

 

뒤의 .bind(this) 는 원래 function 안에선 this의 값이 아무것도 세팅되어있지 않은 상태이기 때문에 this 가 undefined 상태라는 에러가 뜬다. 이처럼 이벤트를 설치할 때 this를 찾을 수 없어 에러가 발생할 때 함수가 끝난 직후에 .bind(this)를 사용해주면  그 함수 안에서 this는 우리의 component를 가리키게 된다.

 

또한, 이미 컴포넌트가 생성된 이후에 동적으로 state를 변경하기 위해선 setState 함수를 사용해야 한다. this.state.mode='welcome'; 로 변경하는 것은 React 입장에선 몰래 바꾼 셈이다.

 

🌭 컴포넌트 이벤트 만들기

Subject 컴포넌트 이벤트 만들기

위에서 주석 처리했던 Subject 컴포넌트를 살리고 Subject.js에서 가져온 태그와 내용을 주석 처리 혹은 삭제한다. Subject 컴포넌트의 props(속성)에 페이지가 바뀌었을 때를 의미하는 함수인 onChangePage를 정의한다.

 

<Subject
          title={this.state.subject.title}
          sub={this.state.subject.sub}
          onChangePage={function () {
            this.setState({ mode: "welcome" });
          }.bind(this)}
        ></Subject>

 

Subject.js에서 a 태그에 onClick 함수를 추가해 앞서 정의한 onChangePage를 불러온다. 함수 정의 방법은 아까와 같이 파라미터로 e를 주고 e.preventDefault()로 리로드를 방지, bind(this)로 this 값을 세팅해주면 된다.

 

<a
            href="/"
            onClick={function (e) {
              e.preventDefault();
              this.props.onChangePage();
            }.bind(this)}
          >
            {this.props.title}
          </a>

 

TOC 컴포넌트 이벤트 만들기

일단, Subject 컴포넌트와 같은 방법으로 진행한다.

 

<TOC
          onChangePage={function () {
            this.setState({ mode: "read" });
          }.bind(this)}
          data={this.state.contents}
        ></TOC>
<a
            href={"/content/" + data[i].id}
            onClick={function (e) {
              e.preventDefault();
              this.props.onChangePage();
            }.bind(this)}
          >
            {data[i].title}
          </a>

 

위와 같은 리스트 링크를 클릭했을 때 밑 content 영역이 바뀌도록 코드를 수정해야 한다. 먼저, constructor에 selected_content_id를 정의하고 초기값을 설정한다. 초기값이 무엇이든 상관없지만 나는 초기값을 2로 할 것이다. 

 

selected_content_id:2,

 

App.js > render 함수에 App 이 read 모드일 때, selected_content_id 가 달라질 때마다 달라진 id에 맞는 title과 desc 가 출력되도록 한다.

 

else if (this.state.mode == "read") {
      var i = 0;
      while (i < this.state.contents.length) {
        var data = this.state.contents[i];
        if (data.id == this.state.selected_content_id) {
          _title = data.title;
          _desc = data.desc;
          break;
        }
        i = i + 1;
      }
    }

 

App.js > return 함수의 TOC 컴포넌트의 onChangePage 함수에 파라미터로 id를 받고 selected_content_id를 이 id로 바꿔주는 코드를 추가한다. 파라미터로 받는 id는 TOC.js 에서 넣을 것이며 바뀐 selected_content_id는 우리가 보는 페이지의 문구를 바꿔줄 것이다.

 

<TOC
          onChangePage={function (id) {
            this.setState({
              mode: "read",
              selected_content_id: Number(id),
            });
          }.bind(this)}
          data={this.state.contents}
        ></TOC>

 

TOC.js > a 태그에서 data-id를 만들어 data[i].id 값들을 넣어준다. 그리고 onClick 함수 안 this.props.onChangePage(); 함수에 파라미터로 e.target.dataset.id를 준다. 여기서 target은 지금 자신이 속한 컴포넌트를 의미하고 dataset은 컴포넌트 안에서 data 로 시작하는 변수들을 의미, 뒤 id는 방금 정의한 data-id 를 의미한다.

 

<a
            href={"/content/" + data[i].id}
            data-id={data[i].id}
            onClick={function (e) {
              e.preventDefault();
              this.props.onChangePage(e.target.dataset.id);
            }.bind(this)}
          >
            {data[i].title}
          </a>

 

따라서 TOC.js > onChangePage() 파라미터로 e.target.dataset.id 를 주면 App.js > TOC 컴포넌트에서 이 값으로 selected_content_id를 바꿔주는 것이다. 코드를 다음과 같이 바꿔도 동작은 같다. bind 함수의 두 번째 파라미터를 이용하는 방식이다.

 

<a
            href={"/content/" + data[i].id}
            onClick={function (id, e) {
              e.preventDefault();
              this.props.onChangePage(id);
            }.bind(this, data[i].id)}
          >
            {data[i].title}
          </a>

 

🌭 전체 코드

// App.js
import React, { Component } from "react";
import TOC from "./components/TOC";
import Subject from "./components/Subject";
import Content from "./components/Content";
import "./App.css";

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      mode: "welcome",
      selected_content_id: 2,
      welcome: { title: "Welcome", desc: "Hello React!!" },
      subject: { title: "MEOW", sub: "The cat rules the world!" },
      contents: [
        { id: 1, title: "HTML", desc: "HTML is for information" },
        { id: 2, title: "CSS", desc: "CSS is for design" },
        { id: 3, title: "JavaScript", desc: "JavaScript is for interactive" },
      ],
    };
    // this.state = {
    //   content: { title: "HTML", desc: "HTML is HyperText Markup Language." },
    // };
  }
  render() {
    var _title,
      _desc = null;
    if (this.state.mode == "welcome") {
      _title = this.state.welcome.title;
      _desc = this.state.welcome.desc;
    } else if (this.state.mode == "read") {
      var i = 0;
      while (i < this.state.contents.length) {
        var data = this.state.contents[i];
        if (data.id == this.state.selected_content_id) {
          _title = data.title;
          _desc = data.desc;
          break;
        }
        i = i + 1;
      }
    }

    return (
      <div className="App">
        <Subject
          title={this.state.subject.title}
          sub={this.state.subject.sub}
          onChangePage={function () {
            this.setState({ mode: "welcome" });
          }.bind(this)}
        ></Subject>

        <TOC
          onChangePage={function (id) {
            this.setState({
              mode: "read",
              selected_content_id: Number(id),
            });
          }.bind(this)}
          data={this.state.contents}
        ></TOC>

        <Content title={_title} desc={_desc}></Content>
      </div>
    );
  }
}

export default App;
// Subject.js
import React, { Component } from "react";

class Subject extends Component {
  render() {
    return (
      <header>
        <h1>
          <a
            href="/"
            onClick={function (e) {
              e.preventDefault();
              this.props.onChangePage();
            }.bind(this)}
          >
            {this.props.title}
          </a>
        </h1>
        {this.props.sub}
      </header>
    );
  }
}

export default Subject;
// TOC.js
import React, { Component } from "react";

class TOC extends Component {
  render() {
    var lists = [];
    var data = this.props.data;
    var i = 0;
    while (i < data.length) {
      lists.push(
        <li key={data[i].id}>
          <a
            href={"/content/" + data[i].id}
            onClick={function (id, e) {
              e.preventDefault();
              this.props.onChangePage(id);
            }.bind(this, data[i].id)}
          >
            {data[i].title}
          </a>
        </li>
      );
      i = i + 1;
    }
    return (
      <nav>
        <ul>{lists}</ul>
      </nav>
    );
  }
}

export default TOC;