본문 바로가기
컴퓨터 사이언스/TIL 정리

[리액트를 다루는 기술] 컴포넌트, 이벤트

by 메리뉴데이 2022. 8. 13.

리액트의 컴포넌트

 

컴포넌트 선언 방식 1. 함수형 컴포넌트 2. 클래스형 컴포넌트

차이점으로 클래스형 컴포넌트는 state 기능 및 라이프사이클 기능을 사용할 수 있고, 임의 메서드를 정의할 수 있다.

하지만, v16.8 업데이트 이후 함수형 컴포넌트도 Hooks 기능이 도입되여 state와 라이프사이클 API 사용이 가능해졌다 !

리액트 공식 매뉴얼에서는 컴포넌트 새로 작성시 함수형 컴포넌트와 Hooks를 사용하도록 권장하고 있다.

 

클래스형 컴포넌트에는 render 함수가 꼭 있어야 하고, 그 안에서 보여 주어야 할 JSX를 반환해야 한다.

 

ES6 문법에서 화살표 함수는 함수를 표현하는 새로운 방식으로 도입되었다. 이 문법은 주로 함수를 파라미터로 전달할 때 유용하다 !

다만, 일반 함수 표기법과의 차이는 가리키는 this 값이 다르다는 것이다.

일반 함수는 자신이 종속된 객체를, 화살표 함수는 자신이 종속된 인스턴스를 가리킨다.

화살표 함수는 값을 연산하여 바로 반환해야 할 때 사용하면 가독성을 높일 수 있다.

 

객체에서 값을 추출하는 문법을 비구조화 할당이라고 부른다. 이 문법은 구조 분해 문법이라고도 불리며, 함수의 파라미터 부분에서도 사용할 수 있다. 함수의 파라미터가 객체라면 !

 

컴포넌트의 필수 props를 지정하거나 props 타입을 지정할 때 propTypes를 사용한다 !

propTypes를 사용하려면 코드 상단에 import 구문을 사용하여 불러와야 한다.

 import PropTypes from 'prop-types';

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

name 값은 무조건 문자열 형태로 전달해야 된다는 것을 의미

 

propTypes를 지정할 때 뒤에 isRequired를 붙여주면 필수 props로 설정할 수 있다. 

ex. MyComponent.propTypes = {
          name: PropTypes.string,
          favNumber: PropTypes.number.isRequired,

       };

 

 

클래스형 컴포넌트에서 props를 사용할 때는 render 함수에서 this.props를 조회하면 된다. 

class MyComponent extends Component {
    render() {
       const { name, favNumber, children } = this.props;    // 비구조화 할당
       return (
           <div>
                안녕하세요, 제 이름은 {name}입니다. <br />
                 children 값은 {children}입니다. <br />
                 제가 좋아하는 숫자는 {favNumber}입니다.
           </div>
        );
    }
}

 

클래스형 컴포넌트에서 defaultProps와 propTypes를 설정할 때 class 내부에서 지정하는 방법도 있다.

 

 

리액트에서 state는 컴포넌트 내부에서 바뀔 수 있는 값을 의미한다.

props는 컴포넌트가 사용되는 과정에서 부모 컴포넌트가 설정하는 값이며, 컴포넌트 자신은 해당 props를 읽기 전용으로만 사용할 수 있다. props를 바꾸려면 부모 컴포넌트에서 바꿔줘야 한다. 

 

리액트에는 두 가지 종류의 state가 있다.

하나는 클래스형 컴포넌트가 지니고 있는 state,

하나는 함수형 컴포넌트에서 useState라는 함수를 통해 사용하는 state이다.

 

클래스형 컴포넌트에 state를 설정할 때는 constructor 메서드를 작성해 설정한다.

constructor(props) {
    super(props);      // 현재 클래스형 컴포넌트가 상속받고 있는 컴포넌트가 지닌 생성자 함수를 호출해 줌.
    this.state = {        // state의 초깃값 설정하기
        number: 0;
    };
}


아니면 이렇게도 설정 가능
state = {
    number: 0,
};

 

현재 state를 조회할 때는 this.state를 조회하면 된다.

이벤트로 설정할 함수를 넣어줄 때 함수 내부에 this.setState라는 함수를 사용한다.

이 때 인자로 전달된 객체 안에 들어 있는 값만 바꾸어준다.

 

this.setState를 사용하여 state 값을 업데이트할 때는 상태가 비동기적으로 업데이트된다.

바로 state 값이 바뀌지 않기 때문에, this.setState를 사용할 때 객체 대신에 함수를 인자로 넣어 줘라 !

<button
    onClick={() => {
        this.setState(prevState => {
            return {
                number: prevState.number + 1
            };
         });
         // 위 코드와 아래 코드는 완전히 똑같은 기능
         // 아래 코드는 함수에서 바로 객체를 반환한다는 의미
         this.setState(prevState => ({
             number: prevState.number + 1
         }));
    }}
>
    + 1
</button>   

 

setState를 사용하여 값을 업데이트하고 난 후에 특정 작업을 하고 싶을 때는 setState의 두 번째 파라미터로 콜백 함수를 등록하여 작업을 처리할 수 있다. 

 

<button
    onClick={() => {
        this.setState(
            {

                number: number + 1
            },
         () => {

         console.log('방금 setState가 호출되었습니다.');
         console.log(this.state);
          }
        );
    }}
>
   +1
</button>

 

 

리액트 16.8 버전부터는 useState 함수를 사용하여 함수형 컴포넌트에서도 state를 사용할 수 있게 되었다.

 

useState 함수의 인자에는 상태의 초기값을 넣어준다. 클래스형 컴포넌트와는 달리 state 초기값이 반드시 객체가 아니어도 된다.

숫자일 수도, 문자열일 수도, 객체일 수도, 배열일 수도 있다.

useState 함수는 호출하면 배열이 반환되는데, 배열의 첫 번째 원소는 현재 상태이고, 두 번째 원소는 상태를 바꿔 주는 함수(Setter 함수라고 부름)이다. 배열 비구조화 할당을 통해 이름을 자유롭게 정해줄 수 있다. 

useState는 한 컴포넌트에서 여러 번 사용해도 상관없다. 

 

클래스형이든 함수형이든 컴포넌트에서 state 값을 바꾸어야 할 때, setState 혹은 useState를 통해 전달받은 세터 함수를 사용해야 한다. 배열이나 객체 사본을 만들고 그 사본에 값을 업데이트한 후, 그 상태를 setState나 세터 함수를 통해 업데이트 한다.

 

const object = { a: 1, b: 2, c: 3 };
const nextObject = { ...object, b: 5 };    // 사본을 만들어 b 값만 덮어 쓰기

const array = [
    { id: 1, value: true },
    { id: 2, value: true },
    { id: 3, value: false },
];
let nextArray = array.concat( { id: 4 } );    // 새 항목 추가
nextArray.filter(item => item.id !== 2);    // id가 2인 항목 제거
nextArray.map(item => (item.id === 1 ? { ...item, value: false} : item));    // id가 1인 항목의 value를 false로 설정

 

객체에 대한 사본을 만들 때는 spread 연산자를 사용하여 처리하고, 배열에 대한 사본을 만들 때는 배열의 내장 함수들을 활용한다 !

 

 

 

 

리액트에서의 이벤트

 

1. 이벤트를 설정할 때, 함수 형태의 객체를 전달한다.

2. 직접 만든 컴포넌트에는 이벤트를 자체적으로 설정할 수 없고, DOM 요소에만 설정할 수 있다 !

     하지만, 전달받은 props를 컴포넌트 내부의 DOM 이벤트로 설정할 수는 있다.

 

<MyComponent onClick={doSomething} />   
// MyComponent를 클릭할 때 그냥 이름이 onClick인 props를 MyComponent에게 전달해 줄 것임


<div onClick={this.props.onClick}>
    {   }
</div>

 

 

<클래스형 컴포넌트로 작성시>

리액트에서의 이벤트 객체는 SyntheticEvent로 웹 브라우저의 네이티브 이벤트를 감싸는 객체이고,

네이티브 이벤트와 달리 이벤트가 끝나고 나면 이벤트가 초기화되어 정보를 참조할 수 없다.

만약 비동기적으로 이벤트 객체를 참조할 일이 있다면 e.persist() 함수를 호출해 주어야 한다.

 

이벤트를 처리할 때 렌더링을 하는 동시에 함수를 만들어 전달해 주었는데,

이것 대신 함수를 미리 준비하여 전달하면, 즉 onChange와 onClick에 전달한 함수를 따로 빼내서 컴포넌트 임의 메서드를 만들면

성능상 차이는 거의 없지만 가독성을 높일 수 있다.

 

함수가 호출될 때 this는 호출부에 따라 결정되므로, 클래스의 임의 메서드가 특정 HTML 요소의 이벤트로 등록되는 과정에서

메서드와 this의 관계가 끊어져 버리기 때문에,  this를 컴포넌트 자신으로 가리키기 위해서는 메서드를 this와 바인딩(binding)하는 작업이 필요하다. 만약 바인딩하지 않는 경우, this는 undefined를 가리키게 된다.

 

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

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

handleClick() {
    alert(this.state.message);
    this.setState({
        message: ' '
    });
}

 

메서드 바인딩은 생성자 메서드에서 하는 것이 정석이지만, 새 매서드를 만들 때마다  constructor도 수정해야하는 번거로움 때문에,

바벨의 transform-class-properties 문법을 사용하여 화살표 함수 형태로 메서드를 정의하는 방법을 선택할 수도 있다.

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

 

handleChange = (e) => {
    this.setState({
        message: e.target.value
    });
}

handleClick = () => {
    alert(this.state.message);
    this.setState({
        message: ' '
    });
}

 

input이 여러 개일 때는 event 객체를 활용 즉, e.target.name과 같은 값을 사용하는 것이다.

state = {
    username: ' ',
    message: ' '
}

handleChange = (e) => {
    this.setState({
        [e.target.name]: e.target.value   
// 객체 안에서 key(name)를 [ ]로 감싸면 그 안에 넣은 레퍼런스가 가리키는 실제 값이 key 값으로 사용된다.

    });
}

handleClick = () => {
    alert(this.state.username + ': ' + this.state.message);
    this.setState({
        username: ' ',
        message: ' ',
    });
}

...

<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}
/>

 

onKeyPress 이벤트 핸들링은 다음과 같이 작성한다.

 

handleKeyPress = (e) => {
    if(e.key === 'Enter') {
        this.handleClick();
    }
}

...

<input
    type="text"
    name="message"
    placeholder="아무거나 입력해 보세요"
    value={this.state.message}
    onChange={this.handleChange}
    onKeyPress={this.handleKeyPress}
/>

 

 

 

<함수형 컴포넌트로 작성시>

인풋의 개수가 많아질 것 같은 경우를 대비해 e.target.name을 활용해 작성하는 것이 좋을 수 있다.

이 때 useState를 통해 사용하는 상태가 여러 개라면 문자열 대신 객체로 넣는다.

const EventPractice = () => {
    const [ form, setForm ] = useState({
        username: ' ',
        message: ' ',
    });
    const { username, message } = form;    // form.username, form.message를 비구조화 할당으로 작성
    const onChange = e => {
        const nextForm = {
            ...form,    // 기존의 form 내용을 이 자리에 복사한 다음
            [e.target.name]: e.target.value  // 원하는 값을 덮어 씌우기
         };
         setForm(nextForm);
     };

...

<input
    type="text"
    name="username"
    placeholder="사용자명"
    value={username}
    onChange={onChange}
/>

<input
    type="text"
    name="message"
    placeholder="아무거나 입력해 보세요"
    value={this.state.message}
    onChange={onChange}
/>

 

e.target.name 값을 활용하려면, 위와 같이 useState를 쓸 때 인풋 값들이 들어 있는 form 객체를 사용해 주면 된다.