본문 바로가기

리액트 React

리액트 기초

opentutorials.org/course/1

 

생활코딩

hello world 생활코딩의 세계에 오신 것을 환영합니다. 생활코딩은 일반인들에게 프로그래밍을 알려주는 무료 온라인, 오프라인 수업입니다.  어떻게 공부할 것인가를 생각해보기 전에 왜 프로그

opentutorials.org

 

특정 디렉토리를 리액트 앱으로 지정하는 방법

npm 설치 후 리액트 앱 개발용 디렉토리로 이동 후 명령어 create-react-app 입력, npm은 리액트를 사용할 때 필요한 앱스토어 같은 존재
Happy hacking이라는 문구가 보이면 설정 완료(시간이 몇 분 정도 걸릴 수 있음)

 

리액트 앱 실행하는 방법

VS Code에서 상단 메뉴 바에서 View->Appearance->Show Panel을 선택하면 Terminal 사용 가능(하단에 패널을 띄우는 방법은 많음, 명령 프롬프트와 완전히 같은 역할)
npm run start 혹은 npm start를 입력하여 리액트 앱을 실행하고 Ctrl+c를 입력하면 일괄 작업을 끝내시겠습니까 (Y/N)? 문구가 뜨고 Y를 입력해 앱을 종료할 수 있음
리액트 앱을 실행했을 때 나타나는 샘플 앱

 

사용자 정의 컴포넌트

App.js가 App이라는 컴포넌트에 대해 정의하는 부분. import App from './App'과 같이 가져와 사용 가능

App.css는 App.js에 대한 css 파일이다.

index.js에서 사용자 정의 컴포넌트를 사용하여 앱을 구성한다. index.html과 index.css도 마찬가지로 index.js에 대한 파일이다.

 

리액트 앱 빌드

명령어 npm run build를 입력하면 배포 가능한 상태로 빌드되고 build라는 디렉토리가 생성됨

디렉토리 build 안에 있는 index.html 등의 파일을 보면 기존의 파일에 비해 불필요한 부분이 지워져 있거나 주석 처리되어 있다.

배포했을 때 사용자에게 필요하지 않은 부분은 생략하는 것이다.

명렁어 npx serve -s build를 입력하면 npm이 제공하는 serve라는 서버를 구축할 수 있음

※ npm이 아니라 npx

-s build는 build라는 디렉토리에 서버를 설치한다는 의미(?)

서버 설치 후 나오는 두 주소 중 아무거나에 접속하면 된다.

localhost:3000은 빌드하지 않은 것이고 localhost:5000은 빌드한 것

새로고침 했을 때 빌드하지 않은 앱은 1.6MB의 리소스를 갖고 빌드한 앱은 144kB의 리소스를 갖는다.

1MB = 2^10kB = 1024kB

빌드한 앱은 개발자에게만 필요하고 사용자에게는 필요하지 않은 코드를 생략하여 용량을 줄인다.

 

리액트를 사용하는 이유

youtu.be/QG4RxNHz-bc

리액트가 없다면

 

리액트 사용 전후 코드 비교

리액트 사용 전

<html>
    <body>
        <header>
            <h1>WEB</h1>
            world wide web!
        </header>

        <nav>
            <ul>
                <li><a href="1.html">HTML</a></li>
                <li><a href="2.html">CSS</a></li>
                <li><a href="3.html">Javascript</a></li>
                
            </ul>
        </nav>

        <article>
            <h2>HTML</h2>
            HTML is HyperText Markup Language.
        </article>
    </body>
</html>

리액트 사용 후

import React, { Component } from 'react';
import './App.css';

class Subject extends Component {
  render() {
    return (
      <header>
            <h1>{this.props.title}</h1>
            {this.props.sub}
        </header>
    );
  }
}

class TOC extends Component { // Teble of Contents
  render() {
    return (
      <nav>
            <ul>
                <li><a href="1.html">HTML</a></li>
                <li><a href="2.html">CSS</a></li>
                <li><a href="3.html">Javascript</a></li>
                
            </ul>
        </nav>
    );
  }
}

class Content extends Component {
  render() {
    return(
      <article>
            <h2>{this.props.title}</h2>
            {this.props.desc}
        </article>
    );
  }
}

class App extends Component {
  render() {
    return (
      <div className="App">
        <Subject title="WEB" sub="world wide web!"></Subject>
        <Subject title="React" sub="For UI"></Subject>
        <TOC></TOC>
        <Content title="HTML" desc="HTML is HyperText Markup Language."></Content>
      </div>
    );
  }
}

export default App;

원래 텍스트로 되어 있던 부분을 {this.props.title}처럼 변경하여 함수처럼 사용하는 것을 리팩토링이라고 한다.

 

컴포넌트를 파일로 분리

import React, { Component } from 'react'; // 리액트로 코딩할 때 필수적인 부분

class TOC extends Component {
    render() {
      return (
        <nav>
              <ul>
                  <li><a href="1.html">HTML</a></li>
                  <li><a href="2.html">CSS</a></li>
                  <li><a href="3.html">Javascript</a></li>
                  
              </ul>
          </nav>
      );
    }
  }

  export default TOC;

src 디렉토리 안에 components 폴더를 만들고 컴포넌트 파일을 넣음

App.js에서 import (컴포넌트 이름) from"./components/(컴포넌트 파일)";을 작성하면 컴포넌트를 사용할 수 있다.

여기서 컴포넌트 이름은 컴포넌트 파일에서 export할 때 지정해준 이름이다.

 

State

App.js의 코드를 constructor를 사용하여 수정했다.

constructor에는 초기값으로 사용할 값을 작성해둔다.

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

class App extends Component {
  constructor(props) { // 가장 먼저 실행돼서 초기화하는 부분
    super(props);
    this.state = { // 내부적인 부분
      subject:{title:'WEB', sub:'World Wide Web!'}
    }
  }
  render() {
    return (
      <div className="App">
        <Subject 
        title={this.state.subject.title} 
        sub={this.state.subject.sub}>
        </Subject>
        <TOC></TOC>
        <Content title="HTML" desc="HTML is HyperText Markup Language."></Content>
      </div>
    );
  }
}

export default App;

chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=ko

 

React Developer Tools

Adds React debugging tools to the Chrome Developer Tools. Created from revision fed4ae024 on 7/15/2020.

chrome.google.com

크롬 확장 프로그램인 React Developer Tools를 다운로드 받으면

React Developer Tools를 다운로드 받으면 리액트 앱에서 검사 도구를 켰을 때 Components와 Profiler라는 탭이 생김

이와 같이 컴포넌트에 대한 내부 정보(State)를 볼 수 있다.

Elements 탭에서는 App.js의 코드만 보여주기 때문에 컴포넌트의 이름(<컴포넌트></컴포넌트>)만 볼 수 있다.

 

State 사용 예시

App.js

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

class App extends Component {
  constructor(props) { // 가장 먼저 실행돼서 초기화하는 부분
    super(props);
    this.state = { // 내부적인 부분
      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() {
    return (
      <div className="App">
        <Subject 
        title={this.state.subject.title} 
        sub={this.state.subject.sub}>
        </Subject>
        <TOC data={this.state.contents}></TOC>
        <Content title="HTML" desc="HTML is HyperText Markup Language."></Content>
      </div>
    );
  }
}

export default App;

props는 properties의 줄임말이고 constructor에서 subject와 content는 각각 하나의 prop이다.

content의 경우처럼 같은 속성(Attribute)에 대해 여러 가지 값을 가질 때 배열(Array)처럼 작성한다.

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}>{data[i].title}</a></li>)
        i = i + 1;
      }
      return (
        <nav>
              <ul>
                  {lists}
              </ul>
          </nav>
      );
    }
  }

  export default TOC;

리액트에서 <li> 태그에 key 값이 없으면 콘솔에 에러 메시지가 뜬다.

태그의 속성 값이 따옴표 한 쌍으로 묶이지 않는 경우(jsx(js) 문법을 사용하는 경우)에는 중괄호({})로 묶어야 한다.

 

이벤트

리액트는 state가 바뀔 때마다 render를 다시 호출한다. 이는 각 컴포넌트 코드에 console.log를 사용하여 확인해볼 수 있다.

App.js에서 아래와 같은 부분을

<Subject 
  title={this.state.subject.title} 
  sub={this.state.subject.sub}>
</Subject>
<header>
  <h1><a href="/" onClick={function(e){
    console.log(e);
    e.preventDefault(); // 기본적인 동작을 막음
    // this.state.mode = 'welcome'; // 동작하지 않음
    this.setState({
    	mode:'welcome'
  	});
  }.bind(this)}>{this.state.subject.title}</a></h1>
  {this.state.subject.sub}
</header>

이와 같이(Subject.js의 구조와 같이) 바꿔준다.

e.preventDefault는 현재 태그(<a>)의 역할인 지정된 주소로 이동하는 동작을 막는다.(기본 역할 외에 다른 이벤트를 적용할 때 주로 사용)

this.state.mode = 'welcome'의 경우 this에 아무 값도 세팅되어 있지 않다. 이 상태로 실행하면 this를 찾을 수 없다는 에러가 뜨는데 이는 함수 끝에 .bind(this)를 붙여주면 해결이 된다.

하지만 이렇게 해서는 리액트가 state가 바뀐 걸 인식하지 못한다. 동적으로 작동하게 하려면 this.setState를 사용해야 한다.

※ 참고: debugger; 라는 코드를 넣으면 웹 브라우저에서 개발자 도구를 켰을 때 그 지점에서 실행을 멈춤

※ 참고: bind 함수 이해하기(다시 공부)

youtu.be/o7Id7GMcuFo

 

컴포넌트 이벤트 만들기

onChangePage라는 이벤트를 만들어 보자.

App.js

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

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!!'}, // status가 바뀌면 render 함수가 다시 호출됨
      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');
    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') {
      _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}
          onChangePage={function(){
            this.setState({mode:'welcome'});
          }.bind(this)}>
        </Subject>
        <TOC data={this.state.contents}></TOC>
        <Content title={_title} desc={_desc}></Content>
      </div>
    );
  }
}

export default App;

onChangePage 이벤트를 state를 바꿔주는 함수로 정의했다.

※ 참고: ===

developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Comparison_Operators

 

비교 연산자

JavaScript는 엄격한 비교와 형변환 비교 두 가지의 비교 방법을 모두 가지고 있습니다. 엄격(일치) 비교(===)는 두 피연산자가 같은 자료형에, 그 내용도 일치해야만 참입니다. 추상(동등) 비교(==)는

developer.mozilla.org

Subject.js

import React, { Component } from 'react';

class Subject extends Component {
    render() {
      console.log('Subject 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;

<a> 태그에 onClick 이벤트를 추가해준다.

우선 <a> 태그의 원래 역할을 막고 onChangePage 이벤트를 호출한다.

함수 맨 끝에 .bind(this) 쓰는 거 잊지 말 것!

WEB을 누르면 state가 바뀌면서 리로드 되어 글자가 Welcome으로 바뀜

App.js에서 TOC의 코드를 변경한다.

        <TOC 
          onChangePage={function(){
            alert('hi');
            this.setState({mode:'read'});
          }.bind(this)} 
          data={this.state.contents}>
        </TOC>

TOC.js

import React, { Component } from 'react';

class TOC extends Component {
    render() {
      console.log('TOC 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(e){
                e.preventDefault();
                this.props.onChangePage();
              }.bind(this)}>
              {data[i].title}</a>
        </li>)
        i = i + 1;
      }
      return (
        <nav>
              <ul>
                  {lists}
              </ul>
          </nav>
      );
    }
  }

  export default TOC;

브라우저에서 검사->Components->App->state를 보면 WEB을 누른 후 컨텐트 목록 중 하나를 눌렀을 때 mode가 'welcome'에서 'read'로 바뀌는 것을 볼 수 있음

컨텐트 목록 중 하나를 클릭했을 때 hi라고 알림창이 뜨면 정상 작동되는 것이다.

이제 컨텐트를 선택했을 때 각 컨텐트 별로 내용이 바뀌게 해보자.

App.js의 TOC 컴포넌트

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

onChangePage 함수의 매개변수로 id를 받는다. 이 id에 따라 selected_content_id의 상태를 바꿔준다.

※ 참고: Number는 js에서 숫자인 문자열을 숫자로 형변환해줌

developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Number

 

Number

Number 객체는 숫자 값으로 작업할 수 있게 해주는 래퍼wrapper 객체입니다. Number 객체는 Number() 생성자를 사용하여 만듭니다. 원시 숫자 자료형은 Number() 함수를 사용해 생성합니다.

developer.mozilla.org

TOC.js

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

 

접두사로 'data-'가 붙은 변수는 dataset을 의미한다.

e.target을 이용해 e(이벤트)가 소재한 태그를 알 수 있다.

따라서 onChangePage에 매개변수로 넘겨 준 e.target.dataset.id는 a 태그 안의 data-id의 값이다.

(onClick 이벤트가 실행 중일 때 debugger를 이용해 멈추면 콜솔에 e를 입력하고 a->dataset->id를 확인할 수 있다(?).)

debugger를 사용해 매개변수로 id 값이 잘 넘어온 것을 확인할 수 있음

※ 참고: 검사 창에서 ESC를 누르면 추가로 Console을 띄울 수 있음

각 컨텐트를 클릭하면 selected_content_id의 값이 바뀜

※ 참고: bind 사용법에 대한 추가 내용

youtu.be/11mTvRtXx4g?t=665

 

베이스 캠프

컴포넌트에서 props를 변경하려고 할 때

import React, { Component } from 'react';

class Content extends Component {
    render() {
      this.props.title = 'hi'; // 에러
      console.log('Content render');
      return(
        <article>
              <h2>{this.props.title}</h2>
              {this.props.desc}
          </article>
      );
    }
  }

  export default Content;

props는 수정이 불가능한 값(read-only)이기 때문에 다섯번째 줄처럼 쓰면 에러가 난다.

상위 컴포넌트에서 하위 컴포넌트의 state를 바꾸려면 props를 사용하고 하위 컴포넌트에서 상위 컴포넌트의 state를 바꾸려면 이벤트를 사용해야 한다.

App.js

	<TOC 
          onChangePage={function(id){ // 하위 컴포넌트에서 상위 컴포넌트의 state 변경
            this.setState({
              mode:'read',
              selected_content_id:Number(id)
            });
          }.bind(this)} 
          data={this.state.contents}> // 상위 컴포넌트에서 하위 컴포넌트 state 변경(?)
        </TOC>

 

※ 참고: 베이스 캠프

youtu.be/b9IaQ3Dkucw

 

Create 구현

create, update, delete 기능을 구현해보자.

create를 클릭하면 기존에 각 컨텐트의 설명이 뜨던 부분에 새 컨텐트를 추가할 수 있는 폼이 뜨게 할 것이다.

확실히 구분하기 위해 기존의 Content.js를 ReadContent.js로 변경해주었다.

CreateContent.js

import React, { Component } from 'react';

class CreateContent extends Component {
    render() {
      console.log('CreateContent render');
      return(
        <article>
          <h2>Create</h2>
          <form>
            
          </form>
        </article>
      );
    }
  }

  export default CreateContent;

App.js

  render() {
    console.log('App render');
    var _title, _desc, _article = null;
    if(this.state.mode === 'welcome') {
      _title = this.state.welcome.title;
      _desc = this.state.welcome.desc;
      _article = <ReadContent title={_title} desc={_desc}></ReadContent>
    }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;
      }
      _article = <ReadContent title={_title} desc={_desc}></ReadContent>
    } else if(this.state.mode === 'create') {
      _article = <CreateContent></CreateContent>
    }
    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>
        <Control onChangeMode={function(_mode){
          this.setState({
            mode:_mode
          });
        }.bind(this)}></Control>
        {_article}
      </div>
    );
  }

_article이라는 변수를 추가해서 mode의 상태에 따라 _article에 컴포넌트를 바꿔가며 담아 띄운다.

이제 내용을 전달할 수 있는 form 태그를 완성해보자.

CreateContent.js

    	<form action="/create_process" method="post"
            onSubmit={function(e){
              e.preventDefault(); // 원래는 화면이 이동됨
              alert('Submit!!!!!');
            }.bind(this)}>
            <p><input type="text" name="title" placeholder="title"></input></p>
            <p>
              <textarea name="desc" placeholder="description"></textarea>
            </p>
            <p>
              <input type="submit"></input>
            </p>
          </form>

form 태그는 원래 html에서 사용되는 태그이고 submit 했을 때 action 값의 주소로 화면을 전환한다.

onSubmit은 submit 버튼을 눌렀을 때 실행되는 함수이다. (함수 뒤에 .bind(this) 잊지 말 것!)

원래 form 태그의 기능은 submit 했을 때 화면이 전환되면서 값을 넘겨주는 것이므로 e.preventDefault()로 막아준다. (화면 전환 없이 동적으로 현재 화면이 변하게 하는 것이 리액트의 특징)

테스트를 위해 App.js의 mode 값을 create로 바꿔준 뒤 실행해본다.

제출 버튼을 누르면 alert가 실행되고 페이지가 이동되지 않음

onSubmit을 테스트하기 위해 코드를 추가해준다.

CreateContent.js

onSubmit={function(e){
              e.preventDefault();
              this.props.onSubmit(
                e.target.title.value,
                e.target.desc.value
              );
              alert('Submit!!!!!');
            }.bind(this)}

 

App.js

else if(this.state.mode === 'create') {
      _article = <CreateContent onSubmit={function(_title, _desc){
        // add content to this.state.contents
        console.log(_title, _desc);
      }.bind(this)}></CreateContent>
    }

Create 기능으로 입력한 값이 콘솔에 출력됨

새로 등록된 컨텐츠가 뜨도록 이어서 코드를 추가해보자.

  ...
  
  constructor(props) {
    super(props);
    this.max_content_id = 3; // UI에 영향이 없는 값은 state에 쓰지 않는 편이 좋음, 렌더링 오래 걸림
    this.state = {
      mode:'create',
      selected_content_id:2,
      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'}
      ]
    }
  }

...

   else if(this.state.mode === 'create') {
      _article = <CreateContent onSubmit={function(_title, _desc){
        // add content to this.state.contents
        this.max_content_id = this.max_content_id+1;
        // this.state.contents.push(
        //   {id:this.max_content_id, title:_title, desc:_desc}
        // );
        var _contents = this.state.contents.concat(
          {id:this.max_content_id, title:_title, desc:_desc}
        )
        this.setState({
          contents:_contents
        });
        console.log(_title, _desc);
      }.bind(this)}></CreateContent>
    }

되도록이면 원본을 수정하는 방식으로 코드를 작성하지 않는 것이 좋다.

값이 추가된 것을 확인할 수 있음

(테스트용 alert는 지웠다.)

 

TOC가 바뀌지 않아도 TOC render가 호출됨

불필요한 렌더링을 줄여보자.

TOC.js

    shouldComponentUpdate(newProps, newState){
      console.log('===>TOC render shouldCompnentUpdate'
        ,newProps.data
        ,this.props.data
      );
      if(this.props.data === newProps.data){
        return false;
      }
      return true;
    }

render 앞에 이 코드를 추가한다.

shouldComponentUpdate는 말 그대로 컴포넌트가 업데이트 되어야하는지 아닌지를 알려주는 역할을 한다.

newProps를 통해서 새로 바뀌어 컴포넌트에 적용되어야할 데이터를 알 수 있다.

여기서 this.props.data는 컴포넌트에 현재 적용되어 있는 데이터이다.

앞서 '원본을 수정하지 않는 방식'으로 코드를 작성했기 때문에 this.props.data와 newProps.data를 비교하는 방법을 사용할 수 있는 것이다. 만약 원본(this.props.data)을 수정했으면 newProps.data와 값이 같아지기 때문에 컴포넌트를 업데이트해야 하는지 아닌지 알기가 어려워진다.

 

Array.from을 이용해서 코드를 고쳐보자.

App.js

   else if(this.state.mode === 'create') {
      _article = <CreateContent onSubmit={function(_title, _desc){
        // add content to this.state.contents
        this.max_content_id = this.max_content_id+1;
        // this.state.contents.push(
        //   {id:this.max_content_id, title:_title, desc:_desc}
        // );
        // var _contents = this.state.contents.concat(
        //   {id:this.max_content_id, title:_title, desc:_desc}
        // )
        var newContents = Array.from(this.state.contents);
        newContents.push({id:this.max_content_id,
        title:_title, desc:_desc});
        this.setState({
          contents:newContents
        });
        console.log(_title, _desc);
      }.bind(this)}></CreateContent>
    }

이 방식도 원본을 바꾸지 않는다.

var b = Array.from(a)라고 쓰면 배열 a의 내용이 b에 복사되고 a에는 전혀 영향이 없다.

※ 객체를 바꾸고 싶으면 Object.assign, 배열을 바꾸고 싶으면 Array.from

developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

 

Object.assign()

Object.assign() 메소드는 열거할 수 있는 하나 이상의 출처 객체로부터 대상 객체로 속성을 복사할 때 사용합니다. 대상 객체를 반환합니다.

developer.mozilla.org

※ 원본 배열을 수정하는지 안 하는지 헷갈린다면 Immutable 라이브러리를 사용할 수도 있다.(배열과 관련된 모든 함수가 원본을 수정하지 않게 해줌)

immutable-js.github.io/immutable-js/

 

Immutable.js

Immutable collections for JavaScript Immutable data cannot be changed once created, leading to much simpler application development, no defensive copying, and enabling advanced memoization and change detection techniques with simple logic. Persistent data

immutable-js.github.io

 

Update 구현

CreateContent.js를 복제해서 UpdateContent.js 파일을 만든다.

UpdateContent.js

import React, { Component } from 'react';

class UpdateContent extends Component {
    render() {
      console.log(this.props.data); // 값이 잘 넘어오는지 테스트
      console.log('UpdateContent render');
      return(
        <article>
          <h2>Update</h2>
          <form action="/create_process" method="post"
            onSubmit={function(e){
              e.preventDefault(); // 원래는 화면이 이동됨
              this.props.onSubmit(
                e.target.title.value,
                e.target.desc.value
              );
            }.bind(this)}>
            <p><input type="text" name="title" placeholder="title"></input></p>
            <p>
              <textarea name="desc" placeholder="description"></textarea>
            </p>
            <p>
              <input type="submit"></input>
            </p>
          </form>
        </article>
      );
    }
  }

  export default UpdateContent;

App.js

getReadContent() {
    var i = 0;
    while(i < this.state.contents.length){
      var data = this.state.contents[i];
      if(data.id === this.state.selected_content_id){
        return data;
        break;
      }
      i = i + 1;
    }
  }
  getContent() {
    var _title, _desc, _article = null;
    if(this.state.mode === 'welcome') {
      _title = this.state.welcome.title;
      _desc = this.state.welcome.desc;
      _article = <ReadContent title={_title} desc={_desc}></ReadContent>
    }else if(this.state.mode === 'read') {
      var _content = this.getReadContent();
      _article = <ReadContent title={_content.title} desc={_content.desc}></ReadContent>
    } else if(this.state.mode === 'create') {
      _article = <CreateContent onSubmit={function(_title, _desc){
        // add content to this.state.contents
        this.max_content_id = this.max_content_id+1;
        // this.state.contents.push(
        //   {id:this.max_content_id, title:_title, desc:_desc}
        // );
        // var _contents = this.state.contents.concat(
        //   {id:this.max_content_id, title:_title, desc:_desc}
        // )
        var newContents = Array.from(this.state.contents);
        newContents.push({id:this.max_content_id,
        title:_title, desc:_desc});
        this.setState({
          contents:newContents
        });
        console.log(_title, _desc);
      }.bind(this)}></CreateContent>
    } else if(this.state.mode === 'update') {
      _content = this.getReadContent();
      _article = <UpdateContent data={_content} onSubmit={function(_title, _desc){
        // add content to this.state.contents
        this.max_content_id = this.max_content_id+1;
        // this.state.contents.push(
        //   {id:this.max_content_id, title:_title, desc:_desc}
        // );
        // var _contents = this.state.contents.concat(
        //   {id:this.max_content_id, title:_title, desc:_desc}
        // )
        var newContents = Array.from(this.state.contents);
        newContents.push({id:this.max_content_id,
        title:_title, desc:_desc});
        this.setState({
          contents:newContents
        });
        console.log(_title, _desc);
      }.bind(this)}></UpdateContent>
    }
    return _article;
  }
  render() {
    console.log('App render');
    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>
        <Control onChangeMode={function(_mode){
          this.setState({
            mode:_mode
          });
        }.bind(this)}></Control>
        {this.getContent()}
      </div>
    );
  }

코드를 보기 좋게 리팩토링 해준다.

기능에 따라 getReadContent()와 getContent() 함수로 분리한다.

 

ko.reactjs.org/docs/forms.html

 

폼 – React

A JavaScript library for building user interfaces

ko.reactjs.org

UpdateContent.js의 form 부분에 코드를 추가하고 리팩토링 해보자.

  ...
  
  constructor(props) {
    super(props);
    this.state = {
      title:this.props.data.title,
      desc:this.props.data.desc
    }
    this.inputFormHandler = this.inputFormHandler.bind(this);
  }
  inputFormHandler(e){
    this.setState({[e.target.name]:e.target.value});
  }
  
  ...
  
            <p>
            <input 
              type="text" 
              name="title" 
              placeholder="title"
              value={this.state.title}
              onChange={this.inputFormHandler}
            ></input>
          </p>
          <p>
            <textarea 
              onChange={this.inputFormHandler}
              name="desc" 
              placeholder="description" 
              value={this.state.desc}>
            </textarea>
          </p>
          
 ...

constructor에서 초기 값을 설정해준다.

바뀐 값을 state에 바로바로 적용할 수 있도록 함수 inputFormHandler를 만든다.

여기서 e.target은 이벤트가 발생하는 태그를 알 수 있게 해주는데 대괄호([])를 사용해 이런 식으로 사용할 수도 있다.(대괄호는 리액트의 최신 문법이라고 함)

함수 끝에 붙는 .bind(this)가 중복되므로 constructor를 이용해서 리팩토링 해줄 수 있다.

이제 폼을 submit했을 때 적용된 내용이 보이도록 해보자.

App.js의 getContent()에 코드를 추가해준다.

  getContent() {
    var _title, _desc, _article = null;
    if(this.state.mode === 'welcome') {
      _title = this.state.welcome.title;
      _desc = this.state.welcome.desc;
      _article = <ReadContent title={_title} desc={_desc}></ReadContent>
    }else if(this.state.mode === 'read') {
      var _content = this.getReadContent();
      _article = <ReadContent title={_content.title} desc={_content.desc}></ReadContent>
    } else if(this.state.mode === 'create') {
      _article = <CreateContent onSubmit={function(_title, _desc){
        this.max_content_id = this.max_content_id+1;
        var newContents = Array.from(this.state.contents);
        newContents.push({id:this.max_content_id, title:_title, desc:_desc});
        this.setState({
          contents:newContents,
          mode:'read',
          selected_content_id:this.max_content_id
        });
        console.log(_title, _desc);
      }.bind(this)}></CreateContent>
    } else if(this.state.mode === 'update') {
      _content = this.getReadContent();
      _article = <UpdateContent data={_content} onSubmit={
        function(_id, _title, _desc){
          this.max_content_id = this.max_content_id+1;
          var newContents = Array.from(this.state.contents); // immutable 테크닉
          var i = 0;
          while(i < newContents.length) {
            if(newContents[i].id === _id) {
              newContents[i] = {id:_id, title:_title, desc:_desc};
              break;
            }
            i = i + 1;
          }
          this.setState({
            contents:newContents,
            mode:'read'
          });
          console.log(_title, _desc);
        }.bind(this)}></UpdateContent>
    }
    return _article;
  }

여기까지 코드를 쓰고 나면 create와 update 기능은 의도한 대로 작동한다.

컨텐츠를 추가하거나 수정할 때 setState로 mode까지 바꿔주는 것을 잊지 말자.

이제 마지막으로 delete 기능을 완성해보자.

App.js

...

        <Control onChangeMode={function(_mode){
          if(_mode === 'delete') {
            if(window.confirm('really?')) {
              var _contents = Array.from(this.state.contents);
              var i = 0;
              while(i < this.state.contents.length) {
                if(_contents[i].id === this.state.selected_content_id) {
                  _contents.splice(i, 1); // 원본 바꿈
                  break;
                }
                i = i + 1;
              }
              this.setState({
                mode:'welcome',
                contents:_contents
              });
              alert('deleted!');
            }
          } else {
            this.setState({
              mode:_mode
            });
          }
        }.bind(this)}></Control>
        
  ...

window.confirm은 alert처럼 팝업창으로 뜬다. (이고잉 선생님이 왜 confirm은 alert랑 다르게 앞에 window.를 붙여야 되냐고 궁금해 하셨다. 나도 궁금)

splice(i, 1)은 i번째부터 한 개 제거하는 함수이다. (splice라는 단어의 사전적 의미와 직결되지는 않는듯..?)

setState로 바뀐 컨텐츠를 적용시키고 mode를 welcome으로 바꿔준다.

 

여기까지 하면 CRUD(create, read, update, delete) 기능이 완성된 것이다!

 

youtu.be/jukwa3_4LdI

"과한 공부는 부족한 공부보다 해롭다. 일단 지금 배운 걸 활용해 보자."

원본이 바뀌는 것은 큰 혼란을 초래할 수 있기 때문에 immutable하게 코드를 짜는 것이 좋다. (immutable.js 사용하는 것도 좋음)

React Router: 주소에 따라 특정 ui를 불러올 수 있도록 할 수 있는 플러그인 같은 기능이라고 함.

reactrouter.com/

 

React Router: Declarative Routing for React

Learn once, Route Anywhere

reactrouter.com

 

npm run eject(create-react-app eject): 마음대로 개발 환경 수정 가능, 되돌릴 수 없음

redux: 중앙에 데이터 저장소를 만들고 모든 컴포넌트가 접근 가능

react server side rendering: 서버에서 웹페이지를 완성한 후에 클라이언트에 띄움

react native: 리액트와 같은 방법으로 native앱(안드로이드와 ios 모두에서 구동 가능)을 만들 수 있음

 

여기까지 24일 걸렸다..!ㅠㅠ 이고잉 선생님 정말 고맙습니다

다들 파이팅!!

'리액트 React' 카테고리의 다른 글

Visual Studio Code 단축키  (0) 2020.09.15
리액트 참고 자료 및 사이트  (0) 2020.09.13