20220926 React 공부
리액트 17버전 & 원리
리액트의 개입 없이 HTML과 JavaScript만으로 이벤트를 처리하는 방식은 다음과 같습니다.
리액트가 등장하기 전, 자바스크립트만으로 웹 프로그램을 만들던 때에는 대부분 이런 방식으로 이벤트 핸들링을 구현했습니다.
STEP1:
Javascript를 사용해서 이벤트 핸들러를 붙일 DOM Node를 Select (getElementById등의 Browser 기본 제공 API를 사용하는 경우가 많았습니다.)
STEP2:
Select된 해당 DOM Node에 addEventListener 함수를 사용해서 특정 콜백 함수를 등록합니다.
STEP3:
사용자가 해당 이벤트 리스너가 바인드된 DOM Node와의 상호작용을 통해 해당 이벤트를 트리거하면, 콜백함수가 동작합니다.
간단한 동작이지만, 이후 소개할 “How React Handles Event(리액트가 이벤트를 관리하는 법)” 와는 다르게 위의 방식은 “이벤트 등록이 필요한 DOM Node에 이벤트를 직접 등록한다” 는 점을 꼭 기억해두셔야 합니다.
리액트 공식문서에 따르면, React 17버전부터 이벤트 핸들러를 “Document” 레벨이 아닌 리액트 트리가 렌더되는 root DOM container에 attach한다고 합니다.
즉, 이벤트 핸들러를 내가 Button 컴포넌트에 붙이든, Input 컴포넌트에 붙이든 상관없이 모든 이벤트 핸들러들이 root DOM container에 부착된다는 의미입니다.
Synthetic Event
리액트는 브라우저 호환(크로스 브라우징)을 위해 NativeEvent를 그대로 사용하는 것이 아닌 SyntheticEvent 객체를 이용해서 NativeEvent를 감싸는 방식을 사용합니다.아래 BaseSyntheticEvent타입(전체 SyntheticEvent 타입의 일부)을 통해 알 수 있듯, 브라우저 NativeEvent를 비롯해, 해당 이벤트를 발생시킨 FiberNode의 정보등을 가지고 있습니다. 실제로 이벤트가 발생했을 때 리액트의 ‘onClick’, ‘onChange’등의 이벤트 핸들러에 전달하는 값은 NativeEvent가 아닌 바로 이 SyntheticEvent 객체가 됩니다.
이 SyntheticEvent를 사용하기 때문에 리액트에서는 이 이벤트를 처리하는 핸들러(‘onClick’, ‘onChange’등)를 따로 보유하게 되는 것이며, NativeEvent(‘click’, ‘change’)와 이 이벤트 핸들러를 매핑해주는 단계가 필요합니다.
Your event handlers will be passed instances of SyntheticEvent, a cross-browser wrapper around the browser’s native event. It has the same interface as the browser’s native event, including stopPropagation() and preventDefault(), except the events work identically across all browsers.: — React Blog
리액트가 처리하는 이벤트 핸들러(‘onClick’, ‘onChange’등)를 root DOM node에 붙이는 과정은 Virtual DOM(Fiber Tree)를 생성하는 시점에 일어납니다. 리액트는 이 Fiber Tree를 root DOM node에 _reactRootContainer 라는 key로 저장하게 되며, 리액트에서 처리하는 이벤트 핸들러들은 이 Fiber Tree가 생성되고 나면, 다음의 단계를 거쳐 root DOM node에 attach 됩니다.
STEP1:
‘click’, ‘change’, ‘dblclick’ 등 핸들링 되어야 할 모든 Native Event들의 목록을 정리합니다. 리액트는 코드 안에 NativeEvent List를 저장해두고 있습니다.
STEP2:
NativeEvent 이름과 리액트 이벤트 핸들러 Property를 매핑합니다. 이를테면 click: ‘onClick’, change: ‘onChange’ 와 같은 형식입니다. 이렇게 생성된 매핑테이블은 추후 이벤트가 발생했을 때, 이를 적절한 이벤트 핸들러와 연결해줍니다.
STEP3:
Discrete Event, UserBlocking Event, Continuous Event 등 리액트에서 정의한 이벤트 타입에 따라 부여하는 이벤트의 우선순위가 다른데, 전체 Native Event를 리액트에서 부여하는 기준에 맞게 우선순위를 설정합니다.
STEP4:
위 1 ~ 3단계를 진행한 이후에 리액트의 Virtual DOM(root Fiber Node)에 이 이벤트 핸들러들을 등록하는 과정을 거칩니다.
코드상으로 특정 컴포넌트(e.g Button Component)에 명시한 이벤트 핸들러라도, 리액트가 Virtual DOM을 생성하는 과정에서 이를 root DOM Node에 등록하기 때문에 실제로 브라우저 API인 getEventListeners() 를 통해 확인해보면 root Node에 모든 이벤트 핸들러들이 붙어 있는 것을 확인할 수 있습니다.
What Happens when User Clicks The Button?
위의 내용을 정리해보면, 리액트는 NativeEvent를 한번 감싼 SyntheticEvent 객체를 사용해 이벤트를 처리하며, FiberTree가 생성되는 시점에 NativeEvent의 이름과 리액트 이벤트 핸들러의 Property를 매핑해주는 매핑 테이블을 생성하고, 전체 NativeEvent에 대해 Loop을 돌면서 해당 이벤트에 리액트 이벤트 핸들러를 등록합니다. (이때 등록되는 EventListener는 dispatchEvent 라는 공통 인터페이스를 사용합니다.)
이 단계들은 앱이 최초로 렌더링 되기 전에 모두 이루어지기 때문에 실제로 사용자가 버튼을 클릭하거나, Input에 값을 입력하는 등의 User Action이 발생하는 시점에는 root DOM node에 모든 이벤트 핸들러가 등록되어 있는 상태입니다.
그렇다면 이 시점에서 유저가 버튼을 ‘클릭’ 했을 때, 실제로 리액트 이벤트 시스템에서는 어떤 일이 일어날까요? 다음의 예시에서 한번 살펴보도록 하겠습니다.
STEP1:
Button을 클릭하면 리액트에서 ‘click’ 이벤트를 감지하고, attach한 이벤트 리스너가 트리거됩니다. 이때, 이 이벤트 리스너는 리액트에서 정의한 dispatchEvent 함수를 호출하게 됩니다.
STEP2:
호출시 넘어온 이벤트 객체로부터 target DOM node(여기서는 Button node)를 식별하며, 내부적으로 사용하는 키인 internalInstanceKey 를 사용하여 이 DOM node가 어떤 Fiber node instance와 매칭되는지를 확인합니다
STEP3:
해당 Fiber node instance를 찾고 나면, 해당 node로부터 출발해서 root node에 이르기까지 Fiber Tree를 순회합니다. 이때 매칭되는 이벤트 Property(‘onClick’)과 매칭되는 이벤트를 가지는 Fiber Node(여기서는 onClick 이 바인딩된 ‘div’ node)를 발견할때 마다 이 이벤트 리스너(콘솔을 찍는 함수)들을 dispatchQueue 라고 불리는 Array에 저장합니다.
STEP4:
root node에 도착하고 나면, 처음 들어간 순서대로 리스너 함수를 실행합니다. (즉 위의 예시에서는 Button의 리스너가 먼저 실행되고, div의 리스너가 나중에 실행됩니다.) 아래 코드에서 주목할 만한 부분은 리스너로부터 fiberNode instance, currentTarget, listener함수를 추출하여 propagation 여부를 검사하고, 이벤트 중복 여부를 확인한 이후에 executeDispatch 함수를 트리거한다는 점입니다.
리액트에서 e.stopPropagation 함수를 호출했을 때, parent Element의 이벤트가 더 이상 전파되지 않는 이유를 확인할 수 있습니다.
Conclusion
리액트 이벤트 시스템은 리액트의 fiberNode 구조(Virtual DOM)와 Synthetic Event 객체를 사용하여 이벤트 핸들링이 필요한 컴포넌트에 직접 이벤트 핸들러를 붙이지 않고도 동작할 수 있도록 구현되었습니다. 리액트 공식 문서에서도 밝혔듯, 17 버전은 17 이후의 버전을 위한 Stepping Stone 이고, 이벤트 핸들링 시스템을 위와 같이 구현함으로써, 하나의 페이지에 서로 다른 버전의 리액트를 적용하더라도, Document 객체가 아닌 각각의 Root Node에 이벤트 핸들러가 바인딩 되므로 쉬운 마이그레이션을 가능하게 해 줍니다.
react 17
리액트는 이벤트 위임(“Event Delegation”) 방식을 통해 이벤트를 처리합니다. 간단히 말해서, 특정 컴포넌트의 버튼에서 클릭 이벤트가 발생하더라도, 실제로 이벤트 핸들러는 해당 버튼에 붙어있는 것이 아닌 root Element (16버전에는 Document, 17버전부터는 root DOM container)에 붙어있게 되는 것입니다.
기존(17버전 이전)에는 규모가 큰 앱의 리액트 버전을 업데이트 할 때, 한번에 업데이트 하지 않고 부분부분 업데이트를 하는 상황, 즉 하나의 애플리케이션에서 서로 다른 버전의 리액트가 존재하는 상황에서 이러한 이벤트 핸들링의 방식이 버그 요인으로 지목 되곤 했습니다. 하나의 애플리케이션에서 서로 다른 두 버전의 리액트가 존재한다고 했을 때, 두 버전의 리액트가 모두 Document Level에 이벤트 리스너를 부착하기 때문에 버그가 생기기 쉬운 환경이 되는 것입니다.
하지만 이번 업데이트에서는 이벤트 리스너를 Document Level이 아닌 root DOM container에 부착함으로써, 서로 다른 버전의 리액트가 각자 자신의 root DOM container에서 이벤트 리스너를 각기 운영할 수 있게 되었습니다. 따라서 이후 리액트 20버전에서 이벤트를 처리하는 방식이 매우 급진적으로 변경되어도 (Dramatic Change) 해당 버전의 이벤트 처리 방식의 적용은 해당 리액트를 사용한 root DOM container의 하위 요소들에만 적용이되고, 기존 17버전의 리액트를 사용한 컴포넌트들은 여전히 안정적으로 17버전의 이벤트 처리 방식을 사용할 수 있게 되는 것입니다. 이것이 리액트 팀에서 이벤트 핸들러를 부착하는 위치의 변경이 “Gradual Upgrade”를 위한 주춧돌이라고 설명한 이유입니다.
Event Pooling
이벤트 풀링에 대한 리액트 공식 문서에 따르면, 리액트 17버전부터는 더 이상 이벤트 풀링이 적용되지 않는다고 설명합니다. 이를 조금 더 이해하기 위해서는 우선 “Event Pooling” 이 무엇인지를 이해해야 합니다.
리액트에서는 이벤트 처리를 위해 브라우저의 native event를 그대로 사용하지 않고 이를 한번 Wrapping한 Synthetic Event를 사용합니다. native event를 한번 Wrapping한 인스턴스를 사용하는 것이기 때문에 이벤트가 발생할 때마다 인스턴스를 생성해야 하며, 이 인스턴스를 저장하기 위한 메모리가 할당됩니다. 또한 이벤트가 처리되고 나면 GC(Garbage Collector)가 이 메모리를 해제해주는 작업도 필요합니다. 실제로 모던 웹 애플리케이션은 수많은 유저 인터렉션을 제공하고, 유저 인터렉션은 이벤트를 발생시킵니다. 여기서 “이벤트가 발생할때마다 Synthetic Event 객체를 생성하고 제거하고 하지 말고, Synthetic Event Pool을 만들어서 이벤트가 발생할때 이 Pool을 사용하면 어떨까?” 라는 아이디어로 만들어진 것이 바로 Event Pool 이라는 개념입니다.
Event Pool은 다음과 같은 방식으로 동작합니다.
- Event Pool에 Synthetic Event Instance를 구비한다.
- 이벤트가 트리거되면 해당 이벤트의 native event는 Event Pool의 인스턴스를 사용해서 Synthetic Event로 래핑한다. (populate its properties)
- 이벤트 핸들러가 종료되어 콜 스택에서 빠져나오면 해당 인스턴스는 초기화되어 다시 풀로 돌아간다.
언뜻 보기에는 메모리도 아낄 수 있고, 불필요하게 GC가 자주 동작할 필요도 없어서 효율적인 시스템인 것 같아 보입니다. 하지만 이벤트가 트리거 되었을 때 인스턴스 풀을 받아오고 이벤트 핸들러가 종료되자마자 다시 인스턴스를 release하는 방식은 필연적으로 비동기 이벤트에 대한 추가적인 대응 (e.persist())을 필요로 했습니다. 따라서 아무런 대응 없이 동기 이벤트와 동일한 방식으로 비동기 이벤트 콜백을 작성하면 문제가 발생했고, 이는 “직관적이지 않은” 개발 경험을 주게 됩니다.
이를테면, 위와 같은 코드를 작성하면 개발 환경에서 리액트는 위와 같은 Warning Message를 전달하며 이를 해결하기 위해 개발자는 e.persist 메서드를 사용해서 별도의 메모리 공간에 해당 이벤트의 Synthetic Event 객체를 할당해야 했습니다. 따라서 실제로 Event Pooling이 성능이 좋아진 모던 브라우저에서는 유의미한 성능 개선을 보여주지 않는 다는 이유와 더불어서 리액트 17버전 부터는 이벤트 풀링이 더 이상 적용되지 않습니다. (기존의 e.persist 메서드는 사용가능하지만, 실제로는 아무것도 수행하지 않습니다). 따라서 17버전부터는 이벤트 핸들러가 동기 함수인지, 비동기 함수인지를 신경쓰지 않고 비즈니스 로직에 집중해서 개발할 수 있게 됩니다.
Conclusion
React Deep Dive — React Event System (1), (2)를 통해 리액트에서 이벤트를 어떤 식으로 처리하는지, 17버전부터는 리액트에서 이벤트를 처리하는 방식이 어떻게 바뀌었는지에 대해 실제 리액트의 구현 코드를 기준으로 살펴보았습니다. 결론적으로는 React 16에서 17로 마이그레이션을 고려하고 계시다면 Event Listener가 더 이상 document Level이 아닌 root DOM container에 붙게 되므로 다른 위치에서 이벤트 핸들링이 일어난다는 점(하지만 비즈니스 로직을 작성하는 방식에는 변화가 없을 것입니다.)과 Event Pooling이 제거되어 비동기와 동기 함수를 같은 방식으로 처리할 수 있으므로 e.persist와 같은 메서드를 호출할 필요가 없다는 점을 추가적으로 고려하면 좋을 것입니다.
[React] 리액트를 다루는 기술
4장 이벤트 핸들링
JSBin(https://jsbin.com/) : 코드 테스트 사이트
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<button onclick = "alert('executed')">
Click Me
</button>
</body>
</html>
import {useState} from 'react';
const Say = () => {
const [message, setMessage] = useState('');
const onClickEnter = () => setMessage('안녕하세요!');
const onClickLeave = () => setMessage('안녕히 가세요!');
const [color, setColor] = useState('black');
return (
<div>
<button onClick = {onClickEnter}>입장</button>
<button onClick = {onClickLeave}>퇴장</button>
<h1>{message}</h1>
</div>
);
};
export default Say;
이벤트를 사용할 때 주의 사항
- 이벤트 이름은 카멜 표기법으로 작성한다. ie. onkeyup -> onKeyUp
❓왜 카멜 표기법.
소문자 html 태그 -
이벤트에 실행할 자바스크립트 코드를 전달하는 것이 아니라, 함수 형태의 값을 전달한다.
(html) 이벤트 설정 시 큰따옴표 안에 실행할 코드를 넣음. (ie.<button onclick = "alert('executed')">
)
(react) 함수 형태의 객체를 전달함. (ie.<button onClick = {onClickEnter}>
)
a. 함수를 랜더링을 하는 동시에 바로 만들어 전달.
b. 함수를 렌더링 외부에 미리 만들어 전달. - DOM 요소에만 이벤트를 설정할 수 있다.
div, button, input, form, span등의 DOM 요소에는 이벤트를 설정할 수 있지만, 우리가 직접 만든 컴포넌트에는 이벤트를 자체적으로 설정할 수 없다.
ex) <MyComponent onClick = {doSomething}/> 에서,
MyComponent를 클릭할 때 domSomething 함수를 실행하는 것이 아니라, 그냥 이름이 onClick인 props(프로퍼티=속성)를 MyComponent에게 전달해 줄 뿐임.❓
따라서 컴포넌트에 자체적으로 이벤트를 설정할 수 없다. 하지만 전달받은 props에 컴포넌트 내부의 DOM 이벤트로 설정할 수는 있다. ie. <div onClick = {this.props.onClick}>
리액트에서 지원하는 이벤트 종류
- Clipboard
- Touch
- Composition
- UI
- KeyBoard
- Wheel
- Focus
- Media
- Form
- Image
- Mouse
- Animation
- Selection
- Transition
- etc. 리액트 매뉴얼(https://facebook.github.io/react/docs/events.html)
예제로 이벤트 핸들링 익히기 - 클래스형 컴포넌트
컴포넌트 생성
EventPractice.js - 클래스형 컴포넌트 생성
import {Component} from 'react';
class EventPractice extends Component {
render() {
return (
<div>
<h1>이벤트 연습</h1>
</div>
);
}
}
export default EventPractice;
App.js에서 생성한 컴포넌트 렌더링
import EventPractice from './EventPractice';
const App = () => {
return <EventPractice />;
};
export default App;
이벤트 핸들링
EventPractice.js - EventPractice 컴포넌트에 input 요소 렌더링, input 요소에 onChange 이벤트 설정.
import {Component} from 'react';
class EventPractice extends Component {
render() {
return (
<div>
<h1>이벤트 연습</h1>
<input
type = "text"
name = "message"
placeholder = "아무거나 입력해 보세요"
onChange = {
(e) => {
console.log(e);
}
}
/>
</div>
);
}
}
export default EventPractice;
콘솔에 기록되는 e 객체 : SyntheticEvent. 웹 브라우저의 네이티브 이벤트를 감싸는 객체.❓
DOM Element의 경우 핸들링 함수에 event=e object를 매개변수로 전달한다. event object를 이용하여 이벤트 발생 원인, 이벤트가 일어난 Element에 대한 정보를 얻을 수 있다. 이벤트 형태(클릭, 키 입력 등)와 DOM 종류(button, form, input 등)에 따라 전달되는 이벤트 object의 내용도 다르니 유의할 것.
네이티브 이벤트와 인터페이스가 같으므로 순수 자바스크립트에서 html 이벤트를 다룰 때와 똑같이 사용하면 됨.❓
SyntheticEvent
이벤트가 끝나고 나면 이벤트 초기화 됨.
❓왜 이벤트가 초기화 되는 것일까?
리액트에서 사용되고 있는 SyntheticEvent는 객체 풀링 방식을 사용한다.(Object Pooling) 매 이벤트마다 해당 객체 사용되는것에 대해서 성능상의 이유로 리액트 에서는 객체 풀링 방식을 사용함으로써 객체 생성 시간을 줄이고 GC(가비지콜렉터)에 대한 노출도 줄이며 메모리관리에 소비되는 시간을 줄이는 방식을 사용하고 있기 때문이다. 그렇기 때문에 객체가 호출되고 난 후에 이벤트 속성이 초기화된다.
비동기적 이벤트 객체 참조를 원한다면 e.persist() 함수 호출해야.
❓비동기적
동기(sync)는 우리가 통상 알고있는 코드가 위에서 밑으로 내려 읽으면서 순차적으로 코드가 적용됨. 비동기(async)는 setTimeout처럼 독자적으로 비동기적으로 발동하거나 ajax 호출의 많은양의 데이터를 가져올때 비동기적으로 가져와서 화면상에 실질적으로 멈춰보이는듯한 증상을 제거할 수 있음
❓콜백함수 = 이벤트함수 콜백함수를 사용하는 이유는 비동기적 프로그래밍을 할 수 있기 때문.
비동기적 테크닉을 사용하는 이유
1.사용자 이벤트 처리
브라우저 화면에서 발생하는 사용자의 이벤트는 예측이 불가능하다. 따라서 이런 화면이벤트를 관리담당하는 녀석에게 우리는 특정이벤트가 발생할 때 호출을 원하는 내용을 callback 함수에 전달하게 된다.
2.네트워크 응답 처리
화면단에서 서버에게 요청을 보냈을 때, 그 응답이 언제 올지 알 수 없다. 따라서 이런 서버에 대한 응답처리 등도 비동기적으로 처리해야 한다.
3.파일을 읽고 쓰는 등의 파일 시스템 작업
4.의도적으로 시간 지연을 사용하는 기능(알람 등)
위와 같이 이벤트 등을 기다리는데 하나뿐인 소중한 스레드를 사용한다면, 또 서버의 응답을 기다리기 위해 하나뿐인 소중한 스레드를 사용한다면… 사용자는 멈춰져 있는 화면을 보게되는 것이다. 위와 같이 스레드의 블록킹을 야기하는 작업은 필수적으로 비동기적 프로그래밍을 해야 한다.
❓e.persist()
EventPractice.js - onChange 코드 수정
onChange = {
(e) => {
console.log(e.target.value);
}
}
수정 후에는 값이 바뀔 때마다 바뀌는 값을 콘솔에 기록함.
❓e.persist()와 e.target.value의 관계
❓e.target.value란?
event=e object의 target은 이벤트의 원인이 되는 Element를 가리킨다. 현재 event의 target은 input element이므로 입력된 value를 가져와 콘솔에 출력하는 모습이다.
state에 input 값 담기
EventPractice.js - constructor에서 state 초깃값 설정, 이벤트 핸들링 함수 내부에서 this.setState 메서드 호출하여 state 업데이트, input의 value 값을 state에 있는 값으로 설정.
import {Component} from 'react';
class EventPractice extends Component {
state = {
message: ''
}
render() {
return (
<div>
<h1>이벤트 연습</h1>
<input
type = "text"
name = "message"
placeholder = "아무거나 입력해 보세요"
value = {this.state.message}
onChange = {
(e) => {
this.setState({
message: e.target.value
})
}
}
/>
</div>
);
}
}
export defalut EventPractice;
❓input의 value 값으로 state 값을 넣어주는 건 어디에 쓰일까. value 값을 비워두면 어떻게 될까.
버튼을 누를 때 comment 값을 공백으로 설정
EventPractice.js - button을 만들고 클릭 이벤트가 발생하면 현재 comment 값을 메시지 박스로 띄운 후 comment 값을 공백으로 설정. (for 입력한 값이 state에 잘 들어갔고 인풋에서 그 값을 제대로 반영하는지 검증.)
import {Component} from 'react';
class EventPractice extends Component {
state = {
message: ''
}
render() {
return (
<div>
<h1>이벤트 연습</h1>
<input
type = "text"
name = "message"
placeholder = "아무거나 입력해 보세요"
value = {this.state.message}
onChange = {
(e) => {
this.setState({
message: e.target.value
})
}
}
/>
<button onClick = {
() => {
alert(this.state.message);
this.setState({
message: ''
});
}
}>확인</button>
</div>
);
}
}
export defalut EventPractice;
❓갑자기 comment는 뭘까. comment는 그냥 입력한 값 의미.
임의 메서드 만들기
리액트에서는 이벤트에 실행할 자바스크립트 코드를 전달하는 것이 아니라, 함수 형태의 값을 전달하는 것이고
a. 함수를 랜더링을 하는 동시에 바로 만들어 전달. ie. onChange = {
(e) => {
this.setState({
message: e.target.value
})
}
}
b. 함수를 렌더링 외부에 미리 만들어 전달. ie. handleChange(e) {
this.setState({
message: e.target.value
});
}
onChange = {this.handleChange}
이렇게 두가지 방식이 있을 수 있는데 앞서 첫번째 방식을 본것이고 이제 두번째 방식을 살펴보자.
EventPractice.js - onChange와 onClick에 전달한 함수를 따로 빼내서 컴포넌트 임의 메서드를 만들어 보자
import {Component} from 'react';
class EventPractice extends Component {
state = {
message: ''
}
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: ''
});
}
render() {
return (
<div>
<h1>이벤트 연습</h1>
<input
type = "text"
name = "message"
placeholder = "아무거나 입력해 보세요"
value = {this.state.message}
onChange = {this.handleChange}
/>
<button onClick = {this.handleClick}>확인</button>
</div>
);
}
}
export defalut EventPractice;
함수가 호출될 때 this는 호출부에 따라 결정되므로, 클래스의 임의 메서드가 특정 HTML 요소의 이벤트로 등록되는 과정에서 메서드와 this의 관계가 끊어져 버린다. 이 때문에 임의 메서드가 이벤트로 등록되어도 this를 컴포넌트 자신으로 제대로 가리키기 위해서는 메서드를 this와 바인딩하는 작업이 필요하다. 만약 바인딩하지 않는 경우라면 this가 undefined를 가리키게 된다. 따라서 정석으로는 생성자 메서드에서 constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.handleClick = this.handleClick.bind(this); } 이렇게 메서드 바인딩을 해준다.
Property Initializer Sysntax를 사용한 메서드 작성
앞서 말했듯이 메서드 바인딩은 생성자 메서드에서 하는 것이 정석이지만 새 메서드를 만들때마다 생성자도 수정해야 하기 때문에 불편할 수 있음. 더 간단하게 하는 방법은 바벨의 transform-class-properties 문법을 사용해 화살표 함수 형태로 메서드를 정의하는 것. EventPractice.js - transform-class-properties 문법 사용해 화살표 함수 형태로 메서드 정의
import {Component} from 'react';
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>
);
}
}
export defalut EventPractice;
이게 화살표 함수를 쓰는 이유인가. 화살표함수와 기본함수의 차이에 this와 관련된게 있었는데 화살표함수는 이렇게 this를 신경쓰지 않고 사용해도 돼서 더 간단하다는 거였나봄.
화살표 함수 형식 어렵게 생각할 필요 없이 보니까 handleChange(e) {} 이 형태에서 handleChange와 (e) 사이에 = 넣고, (e)와 {} 사이에 => 넣어주면 되는 거였음.
input 여러 개 다루기
EventPractice.js
import {Component} from 'react';
class EventPractice extends Component {
state = {
username: '',
message: ''
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
});
}
handleClick = () => {
alert(this.state.username + ': ' + this.state.message);
this.setState({
username: '',
message: ''
});
}
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>
);
}
}
export defalut EventPractice;
handleChange 함수에서 객체 안 key를 []로 감싼것이 핵심인데, 이렇게 되면 그 안에 넣은 레퍼런스가 가리키는 실제 값이 key 값으로 사용된다.
ie. const name = ‘varianKey’;
const object = {
[name] : ‘value’
};
일때, 결과는 ‘varianKey’ : ‘value’ 인것이다.
따라서 handleChange 함수에서 첫번째 인풋이 입력될때는 name이 username으로 정해져있으니까 ([e.target.name] =) e.target.username값으로 입력되고 있는 값(e.target.value)이 들어가고 두번째 인풋이 입력될때는 name이 message로 정해져있으니까 ([e.target.name] =) e.target.message값으로 입력되고 있는 값(e.target.value)가 들어간다.
onKeyPress 이벤트 핸들링
EventPractice.js - 두번째 텍스트 인풋에서 텍스트 입력하고 enter키를 눌렀을 때 handleClick 메서드 호출
import {Component} from 'react';
class EventPractice extends Component {
state = {
username: '',
message: ''
}
handleChange = (e) => {
this.setState({
[e.target.name]: e.target.value
});
}
handleClick = () => {
alert(this.state.username + ': ' + this.state.message);
this.setState({
username: '',
message: ''
});
}
handleKeyPress = (e) => {
if(e.key == 'Enter') {
this.handleClick();
}
}
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}
onKeyPress = {this.handleKeyPress}
/>
<button onClick = {this.handleClick}>확인</button>
</div>
);
}
}
export defalut EventPractice;
예제로 이벤트 핸들링 익히기 - 함수형 컴포넌트
EventPractice.js - 함수형 컴포넌트
import {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 onClick = () => {
alert(userName + ': ' + message);
setUsername('');
setMessage('');
};
const onKeyPress = e => {
if (e.key === 'Enter') {
onClick();
}
};
return (
<div>
<h1>이벤트 연습</h1>
<input
type = "text"
name = "username"
placeholder = "사용자명"
value = {username}
onChange = {onChangeUsername}
/>
<input
type = "text"
name = "message"
placeholder = "아무거나 입력해 보세요"
value = {message}
onChange = {onChangeMessage}
onKeyPress = {onKeyPress}
/>
<button onClick = {onClick}>확인</button>
</div>
);
};
export defalut EventPractice;
여기서도 e.target.name 활용해도 되지만 이번에는 onChange 관련 함수 두개를 따로 만들어줬음.
useState에서 form 객체 사용
for 여러 개의 인풋 상태를 관리하기 위해
EventPractice.js - useState를 통해 사용하는 상태에 문자열이 아닌 form 객체 넣어보자
import {useState} from 'react';
const EventPractice = () => {
const [form, setForm] = useState({
username: '',
message: ''
});
const {username, message} = form;
const onChange = e => {
const nextForm = {
...form, // 기존의 form 내용을 이 자리에 복사한 뒤
[e.target.name]: e.target.value // 원하는 값을 덮어 씌우기
};
setForm(nextForm);
};
const onClick = () => {
alert(userName + ': ' + message);
setForm({
username: '',
message: ''
});
};
const onKeyPress = e => {
if (e.key === 'Enter') {
onClick();
}
};
return (
<div>
<h1>이벤트 연습</h1>
<input
type = "text"
name = "username"
placeholder = "사용자명"
value = {username}
onChange = {onChange}
/>
<input
type = "text"
name = "message"
placeholder = "아무거나 입력해 보세요"
value = {message}
onChange = {onChange}
onKeyPress = {onKeyPress}
/>
<button onClick = {onClick}>확인</button>
</div>
);
};
export defalut EventPractice;
e.target.name 값을 활용하려면 useState를 쓸 때 인풋 값들이 들어 있는 form 객체를 사용해주면 됨.
다음 스터디 : 월요일말고 수요일까지 5장, 6장
Leave a comment