클래스형 컴포넌트

컴포넌트를 선언하는 방식은 두 가지입니다. 함수형 컴포넌트, 클래스형 컴포넌트

컴포넌트 생성

파일 생성 -> 코드 작성하기 -> 모듈 내보내기 및 불러오기

/src/components/MyComponent.js 파일 생성

import React, { Component } from "react";

export default class MyComponent extends Component {
  render() {
    return <div>new Component</div>;
  }
}

/src/App.js 에서 컴포넌트를 불러와서 렌더링해줍니다.

import React, { Component } from "react";
import "./App.css";
import MyComponent from "./components/MyComponent";

export default class App extends Component {
  render() {
    return (
      <div className="mystyle">
        <MyComponent />
      </div>
    );
  }
}

하위 컴포넌트로 데이터 전달하기

/src/App.js

import React, { Component } from "react";
import "./App.css";
import MyComponent from "./components/MyComponent";

export default class App extends Component {
  render() {
    return (
      <div className="mystyle">
        <MyComponent name="react" />
      </div>
    );
  }
}

/src/components/MyComponent.js

import React from "react";

const MyComponent = props => {
  return <div>props = {props.name}</div>;
};

export default MyComponent;

props 기본값 설정: defaultProps

prop가 전달이 안되면 오류가 날거기때문에 기본값을 셋업해주는게 좋습니다.

import React from "react";

const MyComponent = props => {
  return <div>props = {props.name}</div>;
};

MyComponent.defaultProps = {
  name: "기본이름"
};
export default MyComponent;

태그사이의 내용을 보여주는 children

/src/App.js

import React, { Component } from "react";
import "./App.css";
import MyComponent from "./components/MyComponent";

export default class App extends Component {
  render() {
    return (
      <div className="mystyle">
        <MyComponent>리액트</MyComponent>
      </div>
    );
  }
}

/src/components/MyComponent.js

import React from "react";

const MyComponent = props => {
  return (
    <div>
      <h1>props = {props.name}</h1>
      <h2>children = {props.children}</h2>
    </div>
  );
};

MyComponent.defaultProps = {
  name: "기본이름"
};
export default MyComponent;

비구조화 할당 문법을 통해 props 전달받기

destructing assignment이라고 핵 편한 구문입니다.

/src/components/MyComponent.js

import React from "react";

const MyComponent = props => {
  const { name, children } = props;
  return (
    <div>
      <h1>props = {name}</h1>
      <h2>children = {children}</h2>
    </div>
  );
};

MyComponent.defaultProps = {
  name: "기본이름"
};
export default MyComponent;

더 줄이면

import React from "react";

const MyComponent = ({ name, children }) => {
  return (
    <div>
      <h1>props = {name}</h1>
      <h2>children = {children}</h2>
    </div>
  );
};

MyComponent.defaultProps = {
  name: "기본이름"
};
export default MyComponent;

propTypes를 통한 props 검증

컴포넌트의 필수 props를 지정하거나 props의 타입을 지정할 때는 propTypes를 사용합니다.

import React from "react";
import PropTypes from "prop-types";

const MyComponent = ({ name, children }) => {
  return (
    <div>
      <h1>props = {name}</h1>
      <h2>children = {children}</h2>
    </div>
  );
};

MyComponent.defaultProps = {
  name: "기본이름"
};

MyComponent.propTypes = {
  name: PropTypes.string
};

export default MyComponent;

isRequired를 통해 필수 props로 설정하기

MyComponent를 사용하는 곳에서 favoriteNumber를 props로 전달 안하면 경고메시지를 띄워줍니다.

import React from "react";
import PropTypes from "prop-types";

const MyComponent = ({ name, favoriteNumber, children }) => {
  return (
    <div>
      <h1>props = {name}</h1>
      <h2>children = {children}</h2>
      좋아하는 숫자는 {favoriteNumber}
    </div>
  );
};

MyComponent.defaultProps = {
  name: "기본이름"
};

MyComponent.propTypes = {
  name: PropTypes.string,
  favoriteNumber: PropTypes.number.isRequired
};

export default MyComponent;

더 자세한 propTypes 정보는 여기에 있습니다.

클래스형 컴포넌트에서 props 사용하기

import React from "react";
import PropTypes from "prop-types";

class MyComponent extends React.Component {
  render() {
    const { name, favoriteNumber, children } = this.props;
    return (
      <div>
        <h1>props = {name}</h1>
        <h2>children = {children}</h2>
        좋아하는 숫자는 {favoriteNumber}
      </div>
    );
  }
}

MyComponent.defaultProps = {
  name: "기본이름"
};

MyComponent.propTypes = {
  name: PropTypes.string,
  favoriteNumber: PropTypes.number.isRequired
};

export default MyComponent;

state

state란 컴포넌트 내부에서 관리하고 바뀔 수 있는 값을 의미합니다.

props는 컴포넌트가 사용되는 과정에서 부모 컴포넌트가 설정하는 값입니다.
읽기 전용으로만 사용가능합니다.

리액트가 state 사용하는 방법

  • 클래스형 컴포넌트가 지닌 state라는 변수를 통해 사용

  • 함수형 컴포넌트에서 useState라는 함수를 통해 사용

클래스형 컴포넌트가 지닌 state라는 변수를 통해 사용

import React, { Component } from "react";

export default class Counter extends Component {
  constructor(props) {
    super(props);
    //state 초기값 셋업
    this.state = {
      number: 0
    };
  }
  render() {
    const { number } = this.state; // state를 조회할때는 this.state!
    return (
      <div>
        <h1>{number}</h1>
        <button
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있습니다.
            this.setState({ number: number + 1 });
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

state를 contructor없이 쓰기

import React, { Component } from "react";

export default class Counter extends Component {
  state = {
    number: 0
  };
  render() {
    const { number } = this.state; // state를 조회할때는 this.state!
    return (
      <div>
        <h1>{number}</h1>
        <button
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있습니다.
            this.setState({ number: number + 1 });
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

this.setState에 객체 대신 함수 인자 전달하기

this.setState를 사용하여 state 값을 업데이트할 때는 상태가 비동기적으로 업데이트됩니다. 만약 다음과 같이 onClick에 설정한 함수 내부에서 this.setState를 두 번 호출하면 어떻게 될까요?

이거 해보시면 number 2씩 증가 안되는걸 알 수 있습니다.
리액트에서 상태값을 올려주는 메커니즘이 그런거니까 받아들여야 됩니다.

onClick = { () => {
    this.setState({ number: number + 1});
    this.setState({ number: this.state.number + 1 });
}}

굳이 하고 싶으시다면 이렇게 setState부분만 다음과 같이 바꾸면 됩니다.
prevState 기존 상태

onClick = { () => {
    this.setState( (prevState) => {
        return {
            number: prevState.number + 1;
        };
    });

    this.setState( (prevState) => {
        return {
            number: prevState.number + 1;
        };
    });
}}

this.setState가 끝난 후 특정 작업 실행하기

import React, { Component } from "react";

export default class Counter extends Component {
  state = {
    number: 0
  };
  render() {
    const { number } = this.state; // state를 조회할때는 this.state!
    return (
      <div>
        <h1>{number}</h1>
        <button
          onClick={() => {
            // this.setState를 사용하여 state에 새로운 값을 넣을 수 있습니다.
            this.setState({ number: number + 1 }, () => {
              console.log("setState has just called");
            });
          }}
        >
          +1
        </button>
      </div>
    );
  }
}

함수형 컴포넌트에서 useState라는 함수를 통해 state 변경하기

리액트 16.8 이전 버전에서는 함수형 컴포넌트에서 state 사용할 수 없었습니다.
이제는 useState 메서드로 가능합니다.

배열 비구조화 할당 구문을 알고나면 useState 사용 방법을 쉽게 이해할 수 있습니다.

const array = [1,2];
const one = array[0];
const two = array[1];
// 위와 같이 그 동안 할당해왔는데, 다음과 같이 할당할 수가 있게 되었습니다.
const [one, two] = array;

/src/components/Say.js

import React, { useState } from "react";

export default function Say() {
  const [message, setMessage] = useState("");
  const onClickEnter = () => setMessage("hello~");
  const onClickLeave = () => setMessage("good bye~");
  return (
    <div>
      <button onClick={onClickEnter}>Enter</button>
      <button onClick={onClickLeave}>Leave</button>
      <h3>{message}</h3>
    </div>
  );
}

useState함수에는 상태의 초기값을 넣어줍니다.
이 함수의 리턴값을 배열입니다. 0번째 값은 상태, 1번째 값은 상태를 변경하는 함수입니다.

state를 사용할 때 주의 사항

리액트는 state의 변화를 그 state를 참조하는 변수의 참조값 ( 즉, 가상 메모리 주소 )가 바뀌었는지로 판단합니다.

  • 일반 변수의 경우 위와 같이 지정한 setter함수를 사용하여 상태를 변경합니다.

  • 객체의 경우
    spread operator를 사용합니다.
    이렇게 새로운 객체, 새로운 배열을 생성해서 할당해주는 것은 리액트의 가장 중요한 것중 하나죠.

const obj = { a: 1, b: 2, c: 3};
const nextObj = { ...obj, b:2};
  • 배열의 경우
const array = [
  { id: 1, value: true},
  { id: 2, value: true},
  { id: 3, value: false}
  ];
let nextArray = array.concat({id:4}); 

nextArray.filter( el => el.id !== 2); 
nextArray.map( el => el.id === 1? { ...item, value: false} : el);

filter, map같은 Array의 인스턴스 메서드를 사용하여 새로운 배열을 할당해주는 방법으로 구현해야 리액트가 상태값을 추적 메커니즘에 걸려서 렌더링이 다시 되든 상태값이 업데이트되든 합니다.


이벤트 핸들링

사용자가 웹 브라우저에서 DOM 요소들과 상호 작용하는 것을 event라고 합니다.
예를 들어, 버튼을 클릭했 을때는 onClick 이벤트를 실행합니다. Form요소는 값이 바뀔 때 마다 onChange 이벤트를 실행하죠.

이벤트 주의 사항

  1. 리액트에서 이벤트 이름음 camelCase를 사용합니다.
  2. 이벤트를 실행할 자바스크립트 코드를 전달하는 것이 아니라, 함수 형태의 값을 전달합니다.
    2.1 예를 들어, onClick = { () => {} } 이런 식을 넣는다든지, onClick = { 미리 정의해둔 함수 (this바인딩 신경써야합니다) }
  3. DOM 요소에만 이벤트를 설정할 수 있습니다. 컴포넌트에는 이벤트를 달 수 없습니다. 다만 props로 함수를 내려보낼 수 있습니다. 부모 컴포넌트의 state를 변경할 수 있게요.

클래스 컴포넌트에 메서드 만들기

/src/components/EventPractice.js

import React, { Component } from "react";

export default class EventPractice extends Component {
  state = {
    message: ""
  };

  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.setState({
      message: e.target.value
    });
  }
  handleClick() {
    alert(this.state.message);
    this.setState({
      message: ""
    });
  }
  render() {
    return (
      <div>
        <h1>이벤트연습</h1>
        <input
          type="text"
          name="message"
          placeholder="입력하세요"
          value={this.state.message}
          onChange={this.handleChange}
        />
        <button onClick={this.handleClick}>확인</button>
      </div>
    );
  }
}

Property Initializer Syntax를 사용한 메서드 작성

메서드 바인딩은 생성자에서 하는 것이 정석입니다.
더 간단하게 해보기
babel의 transform-class-properties 설정을 통해 화살표 함수로 바꾸면 this를 바인딩해주는 작업이 필요없어집니다.

import React, { Component } from "react";

export default class EventPractice extends Component {
  state = {
    message: ""
  };

  handleChange = e => {
    this.setState({
      message: e.target.value
    });
  };
  handleClick = () => {
    alert(this.state.message);
    this.setState({
      message: ""
    });
  };
  render() {
    return (
      <div>
        <h1>이벤트연습</h1>
        <input
          type="text"
          name="message"
          placeholder="입력하세요"
          value={this.state.message}
          onChange={this.handleChange}
        />
        <button onClick={this.handleClick}>확인</button>
      </div>
    );
  }
}

input 여러개 다루기

handleChange 메서드의 코드를 보면 ES6 구문중
computed property name을 보면 이해가 됩니다.
각괄호 [] 안에 식을 넣을 수 있고, 식이 계산되고 그 결과가 속성명으로 사용됩니다.

import React, { Component } from "react";

export default class EventPractice extends Component {
  state = {
    message: "",
    username: ""
  };

  handleChange = e => {
    this.setState({
      [e.target.name]: e.target.value
    });
  };
  handleClick = () => {
    alert(this.state.username + " : " + this.state.message);
    this.setState({
      message: "",
      username: ""
    });
  };
  render() {
    return (
      <div>
        <h1>이벤트연습</h1>
        <input
          type="text"
          name="username"
          placeholder="사용자명"
          value={this.state.username}
          onChange={this.handleChange}
        />
        <input
          type="text"
          name="message"
          placeholder="입력하세요"
          value={this.state.message}
          onChange={this.handleChange}
        />

        <button onClick={this.handleClick}>확인</button>
      </div>
    );
  }
}

함수형 컴포넌트로 구현해 보기

/src/components/EvenPractice.js

import React, { useState } from "react";

const EventPractice = () => {
  const [username, setUsername] = useState("");
  const [message, setMessage] = useState("");
  const onChangeUsername = e => {
    setUsername(e.target.value);
  };
  const onChangeMessage = e => {
    setMessage(e.target.value);
  };

  const handleClick = e => {
    alert(username + " : " + message);
    setUsername("");
    setMessage("");
  };

  return (
    <div>
      <h1>이벤트연습</h1>
      <input
        type="text"
        name="username"
        placeholder="사용자명"
        value={username}
        onChange={onChangeUsername}
      />
      <input
        type="text"
        name="message"
        placeholder="입력하세요"
        value={message}
        onChange={onChangeMessage}
      />

      <button onClick={handleClick}>확인</button>
    </div>
  );
};

export default EventPractice;

아니면 객체로 상태값을 묶어서 사용할 수도 있습니다.

import React, { useState } from "react";

const EventPractice = () => {
  const [form, setForm] = useState({
    username: "",
    message: ""
  });
  const { username, message } = form;
  const onChange = e => {
    const nextForm = {
      ...form,
      [e.target.name]: e.target.value
    };
    setForm(nextForm);
  };

  const handleClick = e => {
    alert(username + " : " + message);
    setForm({
      username: "",
      message: ""
    });
  };

  return (
    <div>
      <h1>이벤트연습</h1>
      <input
        type="text"
        name="username"
        placeholder="사용자명"
        value={username}
        onChange={onChange}
      />
      <input
        type="text"
        name="message"
        placeholder="입력하세요"
        value={message}
        onChange={onChange}
      />

      <button onClick={handleClick}>확인</button>
    </div>
  );
};

export default EventPractice;

ref: DOM에 이름 달기

일반 HTML에서 DOM요소에 이름을 달 때는 id를 사용합니다.

ReactDOM.render(<컴포넌트>, document.getElementById("root"));
HTML에서 id를 사용하여 DOM에 이름을 다는 것처럼 리액트 프로젝트 내부에서 DOM에 이름을 다는 방법이 있습니다.

그게 바로 ref 개념입니다.

노트

리액트 컴포넌트 안에서 id를 사용할 수 있습니다.
JSX 안에서 DOM에 id를 달면 해당 DOM를 렌더링할 때 그대로 전달됩니다.

HTML에서 DOM id는 유일해야 하는데, 중복 id를 가진 DOM이 여러 개 생긴다면 잘못된 사용입니다.

ref는 전역적으로 작동하지 않고, 컴포넌트 내부에서만 동작하기 때문에 이런 문제가 생기지 않습니다.

id를 사용하지 않고도 원하는 기능을 구현할 수 있지만, 다른 라이브러리나 프레임워크와 함께 id를 사용해야 하는 상황이 발생할 수 있습니다.
그럴땐 id+추가텍스트를 붙여 방지합니다.

ref는 어떤 상황에서 사용해야 할까?

먼저 ref는 어떤 상황에서 사용해야 하는지? DOM을 꼭 직접 건드려야 할 때

  • 특정 input에 포커스 주기
  • 스크롤 박스 조작하기
  • Canvas 요소에 그림 그리기 등

ref 사용

ref를 사용하는 두가지 방법

콜백 함수를 통한 ref 설정

ref를 만드는 가장 기본적인 방법은 콜백 함수를 사용하는 것입니다. ref를 달고자 하는 요소에 ref라는 콜백 함수를 props로 전달해 주면 됩니다. 이 콜백 함수는 ref값을 파라미터로 전달받습니다. 그리고 함수 내부에서 파라미터로 받은 ref를 컴포넌트의 멤버 변수로 설정해 줍니다.

this.input은 input 요소의 DOM을 가리킵니다. ref 이름은 임의로 정할 수 있고, DOM 타입과 관계없이
this.ee = ref 처럼 마음대로 지정합니다.

import React, { Component } from "react";
import "../style/ValidationSample.css";

export default class ValidationSample extends Component {
  state = { password: "", clicked: false, validated: false };
  handleChange = e => {
    this.setState({
      password: e.target.value
    });
  };
  handleButtonClick = () => {
    this.setState({
      clicked: true,
      validated: this.state.password === "0000"
    });
    this.input.focus(); <-
  };
  render() {
    return (
      <div>
        <input
          ref={ (ref) => {this.input = ref} } <-
          type="password"
          value={this.state.password}
          onChange={this.handleChange}
          className={
            this.state.clicked && this.state.validated ? "success" : "failure"
          }
        />
        <button onClick={this.handleButtonClick}>검증하기</button>
      </div>
    );
  }
}

createRef를 통하 ref 설정

리액트에 createRef함수를 이용합니다. 리액트 16.3부터 도입되었으며 이전 버전에서는 동작하지 않습니다.
createRef를 사용하여 ref를 만들려면 우선 컴포넌트 내부에서 멤버 변수로 React.createRef()를 담아 주어야 합니다.
그리고 해당 멤버 변수를 ref를 달고자 하는 요소에 ref props로 넣어 주면 ref 설정이 완료됩니다.
설정한 뒤 ref를 설정해 준 DOM에 접근하려면 this.input.current 를 조회하면 됩니다.

import React, { Component } from "react";
import "../style/ValidationSample.css";

export default class ValidationSample extends Component {
  state = { password: "", clicked: false, validated: false };
  input = React.createRef(); <-
  handleChange = e => {
    this.setState({
      password: e.target.value
    });
  };
  handleButtonClick = () => {
    this.setState({
      clicked: true,
      validated: this.state.password === "0000"
    });
    this.input.current.focus(); <-
  };
  render() {
    return (
      <div>
        <input
          ref={this.input} <-
          type="password"
          value={this.state.password}
          onChange={this.handleChange}
          className={
            this.state.clicked && this.state.validated ? "success" : "failure"
          }
        />
        <button onClick={this.handleButtonClick}>검증하기</button>
      </div>
    );
  }
}

컴포넌트에 ref 달기

리액트에서는 컴포넌트에도 ref를 달 수 있습니다. 이 방법은 주로 컴포넌트 내부에 있는 DOM을 컴포넌트 외부에서 사용할 때 씁니다. 컴포넌트에 ref를 다는 방법은 DOM에 ref를 다는 방법과 똑같습니다.

<MyComponent 
    ref = { (ref) => { this.myComponent = ref }}
/>

MyComponent 내부의 메서드 및 멤버 변수에도 접근할 수 있습니다. 즉, 내부의 ref에도 접근할 수 있습니다. (예: myComponent.handleClick, myComponent.input 등 )

컴포넌트에 메서드 생성

예제를 위한 참고 지식

  • scrollTop : 세로 스크롤바 위치 ( 0~ 350)
  • scrollHeight : 스크롤이 있는 박스 안의 div 높이 ( 650 )
  • clientHeight : 스크롤이 있는 박스의 높이( 300 )

컴포넌트에 ref 달고 내부 메서드 사용

/src/components/ScrollBox.js

import React, { Component } from "react";

export default class ScrollBox extends Component {
  scrollToBottom = () => {
    const { scrollHeight, clientHeight } = this.box;
    console.log(`scrollHeight:${scrollHeight} , clientHeight:${clientHeight}`);

    this.box.scrollTop = scrollHeight - clientHeight;
  };
  render() {
    const style = {
      border: "1px solid black",
      height: "300px",
      width: "300px",
      overflow: "auto",
      position: "relative"
    };

    const innerStyle = {
      width: "100%",
      height: "650px",
      background: "linear-gradient(white, black)"
    };
    return (
      <div
        style={style}
        ref={ref => {
          this.box = ref;
        }}
      >
        <div style={innerStyle}></div>
      </div>
    );
  }
}

/src/App.js

import React, { Component } from "react";
import "./App.css";
import ScrollBox from "./components/ScrollBox";

export default class App extends Component {
  render() {
    return (
      <>
        <ScrollBox
          ref={ref => {
            this.scrollBox = ref; // 콜백방식으로 ref를 달기
          }}
        />
        <button onClick={() => this.scrollBox.scrollToBottom()}>
          맨밑으로
        </button>
      </>
    );
  }
}

시뮬레이션 순서도

App 컴포넌트에서 ScrollBox 컴포넌트 this.scrollBox에 ref를 달기 ->

App 컴포넌트에서 onClick 이벤트 발생 ->

this.scrollBox를 통해 ScrollBox 메서드, 멤버 변수에 접근할 수 있음 그러므로
ScrollBox 컴포넌트 내에 scrollToBottom 메서드를 호출 ->

ScrollBox 내에 scrollToBottom 메서드 호출 ->

ScrollBox의 div 태그에 this.box로 ref를 담았기 때문에 가장 맨 아래로 내려가게끔 내부 메서드를 호출함으로써 this.box.ScrollTop이 맨 아래로 내려가게 할 수 있게됩니다.

scrollHeight :
읽기 전용 속성인 Element.scrollHeight는 수직 스크롤바가 있는 엘리먼트(element)의 CSS 높이를 초과하여, 보이지 않는 부분까지 포함한 내용(content)의 높이(height)입니다
image.png

clientHeight :
https://developer.mozilla.org/ko/docs/Web/API/Element/clientHeight
엘리먼트 높이를 말해요.
높이는 엘리먼트의 높이를 나타내는 픽셀 단위의 integer입니다.

ScrollTop은 스크롤의 맨 위 포인트 지점을 말합니다.