React(생활코딩)_5일차_이벤트의 state, props & render함수

Lina Hongbi Ko·2023년 8월 21일
2

React_생활코딩

목록 보기
6/23

이전시간에 컴포넌트들을 분리하고 state를 써서 생산자의 입장에서는 내부의 구현 정보들을 숨기고, 사용자의 입장에서는 쉽게 원하는 정보를 지정해서 쓸 수 있는 방법을 배웠다.
이제 이벤트에 관해서 배워보려고 한다.

이벤트's state, props

: 이벤트는 어플리케이션을 역동적으로 만들어주는 기술이다.
이 책에 따르면 props, state, event 3개가 서로 상호작용하면서 어플리케이션의 역동성을 만들기 때문에 이 3가지를 함께 생각할 필요가 있다고 했다.

이전 시간의 코드를 가지고 예제를 통해 이벤트를 구현해보려고 한다.

이벤트처리

왼쪽 화면의 WEB을 누르면 아래의 제목과 설명을 그림처럼 바꿔보는(Welcome, Hello,React!!) 이벤트를 설정해보려고 한다. (아래 텍스트 바꾸기) 그리고 각 링크들을 누르면 그에 해당하는 콘텐츠 또한 아래에 바꿔서 출력되게 만들어보는 실습을 해보자. (각 링크누르면 바꾸기)
(App 컴포넌트의 state가 바뀌고, 바뀐 내용이 Content 컴포넌트의 Props로 전달되어서 동적으로 어플리케이션이 바뀌도록 구현)

1. 아래 텍스트 바꾸기

우선, Subject.js파일의 h1 태그 안에 a 링크를 추가해보자.

// Subject.js 파일

... 생략 ...
return(
	<header>
    	<h1><a href="/">{this.props.title}</a></h1>
        {this.props.sub}
    </header>
);
... 생략 ...

그리고 state부터 설정해보자.
책에서는 welcome페이지와 read페이지를 나누고 각 페이지에 맞게 아래의 텍스트가 바뀌도록 설정하도록 하고 있다. 그래서 먼저 현재 페이지가 welcome인지, read인지 구분하기 위해서 state에 mode 라는 프로퍼티를 넣어서 일단 'welcome'을 기본값으로 지정하고, welcome 상태일때의 콘텐츠영역에 표시할 내용 또한 지정해준다.
App.js 파일에 아래처럼 작성해준다.

// App.js 파일

... 생략 ...
class App extends Component {
	constructor(props) {
   	super(props);
       this.state = {
       // mode 작성
       	mode:'welcome',
        subject:{title:"WEB", sub:"World Wide Web!"},
        // welcome 일때 변경 하고 싶은 내용
        welcome:{title:"Welcome", desc: "Hello, React!!"},
        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"}
        ]
           
       }
       ... 생략 ...
   }
}

여기서 생코님이 중요한 사실을 하나 알려주는데, 리액트에서 render 함수는 어떤 HTML을 그릴 것인가라는 것을 결정하는 함수라고 했다. 그리고 state값이 바뀌면 그 state를 가지고 있는 컴포넌트의 render함수가 다시 호출 된다고 한다. 그리고 상위 컴포넌트의 render 함수가 다시 호출되면 하위 컴포넌트들의 render함수 또한 함께 호출되어 화면이 다시 그려진다고 한다.

💡 쉽게 말해, 리액트에서 props나 state값이 바뀌면 그에 해당하는 컴포넌트의 render함수가 호출되도록 약속되어있다는 말이다. -> props나 state가 바뀌면 화면이 다시 그려진다.

이것을 살펴보기 위해 state의 mode값에 따라 컴포넌트의 렌더링 결과가 달라지도록 코드를 작성해보자.

// App.js 파일

... 생략 ...
class App extends Component {
	constructor(props) {
    	super(props);
        this.state = {
        	mode: 'read',
            subject:{title:"WEB", sub:"World Wide Web"},
            welcome:{title:"Welcome", desc: "Hello, React!!"},
         	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"}
         	]
        }
    }
    render() {
    	consle.log("App render");
        let _title, _desc = null;
        if(this.state.mode === "welcome") {
        	_title = this.state.welcome.title;
            _desc = this.state.welcome.desc;
        } else if (this.state.mode === "read") {
        	_title = this.state.contents[0].title;
            _desc = this.state.contents[0].desc;
        }
    	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>
        );
    }
}

export default App;

위의 코드를 살펴보면, state의 mode값이 'welcome'일때와 'read'일때를 조건문으로 나눠서 render함수에 설정했다. 그리고 render 함수 맨 윗줄에는 console.log를 작성해 render함수 호출 순서를 알아보려고 한다.

각 컴포넌트에도 render함수내에 console.log를 실행시켜 호출순서를 알아보면,

// Subject.js 파일

import React, { Component } from 'react';
class Subject extends Component {
	render(){
    	console.log('Subject render');
        return(
          <header>
            <h1>{this.props.title}</h1>
            {this.props.sub}
          </header>
        );
    }
}
// TOC.js 파일

import React, { Component } from 'react';
class TOC extends Component {
	render() {
    	console.log('TOC render');
        let lists = [];
    	const data = this.props.data;
    	for (let i = 0; i < data.length; i++) {
      		lists.push(<li key={data[i].id}><a href={"/content/" + data[i].id}>{data[i].title}</a></li>);
    	}
        return (
          <nav>
            <ul>{lists}</ul>
          </nav>
        );
    }
}
// Content.js 파일
import React, { Component } from 'react';
class Content extends Component {
	render() {
    	console.log('Content render');
        return (
          <article>
            <h2>{this.props.title}</h2>
            {this.props.desc}
          </article>
    	);
    }
}

위처럼 console을 순서대로 확인 할 수 있다.

그리고 개발자도구의 Component탭을 열어서 mode를 'welcome'으로 바꾸고 다시 console창을 보면 state를 바꿨기 때문에 render 함수가 다시 실행되면서 하위 컴포넌트들의 render 함수도 다시 호출되어 cosole이 찍힌 것을 확인할 수 있다.

변경된 state값이 각 컴포넌트의 props값으로 들어오면서 화면이 갱신된다.

이제 개발자도구를 통해 변경하지 않고, 버튼을 클릭했을때 앱의 mode값을 바꿔보자.

// 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",
      welcome: {title: "Welcome", desc: "Hello, React!!"},
      subject: {title:"WEB", sub:"World Wide Web"},
      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"}
         	]
    };
  }
  render() {
    console.log("App render");
    let _title, _desc = null;
    if (this.state.mode === "welcome") {
      _title = this.state.welcome.title;
      _desc = this.state.welcome.desc;
    } else if (this.state.mode === "read") {
      _title = this.state.contents[0].title;
      _desc = this.state.contents[0].desc;
    }
    return (
      <div className="App">
      	// 앞전의 Subject코드는 주석처리
        {/* <Subject
          title={this.state.subject.title}
          sub={this.state.subject.sub}
        ></Subject> */}
        
        // Subject.js내용 추가
        <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>
    );
  }
}

export default App;

원래 있던 Subject 컴포넌트는 일단 주석처리 하고, Subject 컴포넌트의 내용을 직접 App.js에 넣어주었다.
원래 Subject.js 파일 안의 컴포넌트 구성 내용을 보면 {this.props.title}, {this.props.sub} 요렇게 props를 넣어주었지만, App컴포넌트에 바로 넣을 때는 {this.state.subject.title}, {this.state.subject.sub} 요렇게 바꿔주었다.

자, 이제

// App.js 파일

<a href="/">{this.state.subject.title}</a>

위의 링크를 클릭했을때 자바스크립트코드가 실행되게 하려면 리액트의 규칙에 따라 이벤트를 사용한다.
HTML에서 클릭이벤트는 onclick 이지만 리액트에서는 onClick으로 써줘야한다. (카멜법칙을 따름)
그리고 중괄호 { }를 사용해 function을 정의한다. 이름 없는 사용자 정의 함수는 링크를 클릭했을 때 실행되게 약속 되어 있다.

// App.js 파일

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

위처럼 작성하고 'WEB'영역을 클릭하면 alert창이 나타나는 것을 확인할 수 있다.

하지만 alert 창의 확인버튼을 누르면 새로고침된다. 이것은 a태그를 클릭했을때 href 속성의 페이지 이동 동작 때문이다. 이것을 살펴보기 위해 debugger을 넣고 개발자도구를 다시 살펴보면,

// App.js 파일

<a hef="/" onClick={function(e){
	console.log(e);
    debugger;
    alert("hi");
}}>{this.state.subject.title}</a>

여러가지 정보를 확인할 수 있고, onClick안의 함수의 e는 우리가 이벤트를 처리할 수 있게 함수에 주입된 정보이다. event객체라는 소리. 그래서 이 객체 안에 preventDefault라는 메서드를 우리는 사용할 수 있다. preventDefault()는 이벤트가 발생한 태그의 기본적인 동작을 못하게 막는 기능을 수행하는 함수다.

그래서 확인버튼을 누를때마다 새로고침되는 기능을 막기 위해 아래처럼 작성하면, (debugger과 alert를 삭제하고)

// App.js 파일

<a hef="/" onClick={function(e){
	conosle.log(e);
	e.preventDefault();
}}>{this.state.subject.title}</a>

WEB을 클릭했을때, 더이상 새로고침되지 않는 것을 확인할 수 있다.

자, 이제 본격적으로 WEB을 눌렀을때 아래의 텍스트가 바뀌게 하려면 state를 설정해놓은 작업을 연결하기만 하면 된다. 이 말은 a태그를 눌렀을때 App컴포넌트의 state mode값을 'welcome'으로 바꾸면 된다는 소리.

// App.js 파일

class App extends Component {
	... 생략 ...
    <header>
      <h1>
		<a hef="/" onClick={function(e){
        	conosle.log(e);
			e.preventDefault();
            this.state.mode = 'welcome';
		}}>{this.state.subject.title}</a></h1>
        {this.state.subject.sub}
    </header>
}
... 생략 ...

위처럼 this.state.mode="welcome"을 넣어준다.
하지만,
이렇게 해서 실행해보면


요런식으로 에러가 뜬다. this가 undefined라고!
이 말은 this가 컴포넌트 자기자신을 가리키지 않고 있어서 오류가 생기는 것이다. (this binding을 알면 왜 오류가 생기는지 알 것이다. render함수를 제외한 나머지 함수 안의 this는 자기 자신이 아니라 전역객체를 가르키므로 아니다)
그래서 우리는 함수 선언이 끝난 직후에 .bind(this)를 붙여서 this를 바인딩시켜줘야 한다.

// App.js 파일

class App extends Component {
	... 생략 ...
    <header>
      <h1>
		<a hef="/" onClick={function(e){
        	conosle.log(e);
			e.preventDefault();
            this.state.mode = 'welcome';
		}.bind(this)}>{this.state.subject.title}</a></h1>
        {this.state.subject.sub}
    </header>
}
... 생략 ...

위처럼 .bind(this)를 작성해주면 함수 안에서 this는 App 컴포넌트 자기자신을 가르킬 수 있다. 자 이제 이렇게 작성하고, WEB 버튼을 클릭하면 state.mode값이 welcome으로 바뀐다. 하지만 여전히 페이지가 다시 렌더링 되어야 하는데 아무런 반응이 없다.

왜그럴까?

이유는 리액트가 state값이 바뀌었다는 것을 모른다.
작성해주면 리액트가 state값이 바뀌었는지 아닌지 모르기 때문에,

💡 setState()함수를 이용해야 한다.

// App.js 파일

class App extends Component {
	... 생략 ...
    <header>
      <h1>
		<a hef="/" onClick={function(e){
        	conosle.log(e);
			e.preventDefault();
            this.setState({mode:'welcome'});
		}.bind(this)}>{this.state.subject.title}</a></h1>
        {this.state.subject.sub}
    </header>
}
... 생략 ...

위의 코드롤 보면 this.state.mode = 'welcome' 에서 this.setState({mode:'welcome'})으로 코드를 변경해서 작성한 것을 볼 수 있다. 이처럼 리액트에서는 state값을 변경할때 this.setState()함수를 이용해야 한다고 한다. 다시 이렇게 코드를 변경하고 console을 보면 state값이 변경됐기 때문에 각 컴포넌트의 render 함수가 다시 호출되는 것을 볼 수 있고, 텍스트도 바뀌는 것을 확인할 수 있다.

📍📍 constructor은 App 컴포넌트가 생성될 때 최초로 실행된다. 이때 this.state의 초깃값은 기본값으로 지정해주면 되지만 이미 컴포넌트가 생성된 다음에 동적으로 state값을 바꿀때는 this.state값을 직접 수정하면 안된다. 변경하고싶을 때는 그 값을 "객체형태"로 setState() 함수를 이용해서 바꾼다.

this.setState({ mode: 'welcome' });

요렇게 render 함수에서 전달해주면 된다. 아까처럼 this.state.mode = 'welcome' 으로 바꾸면 리액트 입장에서는 몰래 바꿔서 알 수 없게 되어 렌더링 할 수 없다고 한다. 그래서 state를 바꿔야할 때는 반드시 setState() 함수를 사용해야 한다. 그리고 헷갈리지 말아야할 점은 constructor에서는 편하게 기본값으로 작성하면 된다.

💡 자, 정리하면!
state값을 변경하기 위해서는 두가지가 필요하다.

📍 첫번째, 이벤트 함수뒤에 .bind(this) 붙이기
📍 두번째, this.setState() 함수 호출해서 state 값 변경하기

요렇게 state값을 변경해서 바꾸고 싶은 내용이 있는 컴포넌트에 그 값을 전달해 props를 바꾸도록 해보았다. (1. 아래의 텍스트 바꾸기)
다음에는 2번(2. 각 링크를 클릭하면 내용바꾸기)까지 해서 더 내용을 보충해보려고 한다.


출처: 생활코딩! React 프로그래밍 책

profile
프론트엔드개발자가 되고 싶어서 열심히 땅굴 파는 자

0개의 댓글