this는 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키는 자기 참조 변수이다. this를 통해 자신이 속한 객체 또는 자신이 생성할 인스턴스의 프로퍼티나 메서드를 참조할 수 있다.
배경
boostcamp AI Tech 1기에 참여하여 팀 프로젝트로 리액트 앱을 개발한 적이 있었다.
CSV 형태의 평가 데이터를 입력하면 그에 따른 모델 예측 결과를 이미지로 확인하는 애플리케이션이었다.
사용자가 CSV 파일을 입력하고 전송 버튼을 클릭하면 즉시 로딩 화면(스피너)이 보이면서 모델 서버에 요청을 보내게 된다.
서버로부터 응답을 받고 나면 상태 변화에 따라 예측 결과를 화면에 보여준다.
render 메서드의 초기 화면(CSV input, button)
render() {
const { isLoading, infScore } = this.state;
// state가 변경되면 rendering이 다시 일어난다.
// csv file input을 받으면 button onclick 콜백함수로 modelInference가 실행되고
// isLoading state가 true가 되며 로딩화면이 뜬다.
if (isLoading === false) {
return (
<div className="file_upload">
<label className="file_label" htmlFor="file">
📂 Input file(csv) 📂
</label>
<input
id="file"
className="file_input"
type="file"
accept=".csv"
onChange={(event) => {
this.setState({
inputFile: event.target.files[0],
});
}}
/>
<button onClick={this.modelInference}>Start Inference🔎</button>
</div>
);
}
}
그런데 버튼 클릭 후 이벤트 핸들러(modelInference 메서드)가 실행되었을 때, 콘솔 창에 state를 찾지 못한다는 오류 메시지가 떴다.
코드 상에서 전혀 문제가 보이지 않았는데 state를 찾지 못한다니 이해가 되지 않았다.
modelInference() {
// model inference하고 결과 받아오기
const { inputFile } = this.state;
if (inputFile === undefined) {
alert("You forgot data!🤭"); // input없이 화면이 넘어오면 alert
} else {
this.setState({ isLoading: true }); // isLoading state를 true하며 로딩화면으로 렌더링
this.getModelScore(); // 모델 서버와 통신(서버 내에 raw 데이터 생성, 플롯을 response로 받는다.)
}
}
오류 메시지를 토대로 비슷한 사례를 검색하던 중, 공식 문서에서 해답을 찾을 수 있었다.
https://ko.reactjs.org/docs/faq-functions.html#bind-in-render
컴포넌트에 함수 전달하기 – React
A JavaScript library for building user interfaces
ko.reactjs.org
https://ko.reactjs.org/docs/handling-events.html
이벤트 처리하기 – React
A JavaScript library for building user interfaces
ko.reactjs.org
ES6 class와 this, 실행 컨텍스트에 관련된 문제라는 것을 알게 되었고, 자바스크립트 Deep Dive
내용을 토대로 this를 바인딩해주어야 한다는 것을 알게 되었다.
Action
내가 원했던 건 this가 Model 컴포넌트(class)에 바인딩이 되는 것이었다. 만약 그랬다면 의도한 대로 this가 가리키는 Model 클래스 내부의 프로퍼티와 메서드를 사용했을 것이다. 그런데 undefined라면 존재하지 않는(선언하지 않은) 객체를 사용하려고 한다는 것이다.
해결을 위한 핵심적인 키워드만 요약하자면 this는 호출 방식에 따라 바인딩이 된다는 것이다.
일반 함수는 전역 객체 window를 가리키고,
생성자 함수는 생성자 함수가 생성한 인스턴스를 가리키고,
메서드는 메서드를 호출한 객체를 가리킨다.
Stackoverflow에서 한 답변의 문장을 그대로 가져오면 다음과 같다.
You do it because most of the times we are passing the method references either to the event handlers or as props to components. When you pass references of your methods, they become detached functions and their context changes.
클래스 메서드의 this는 메서드를 호출한 객체를 가리키게 된다.
원래는 클래스 인스턴스를 가리키겠지만, 이벤트 핸들러와 같이 메서드를 참조 전달을 하는 경우 해당 메서드만 따로 "떨어져 나와" 전달이 된다. 즉, 본인의 컨텍스트를 잃어버리게 되는 것이다.
이 문제를 해결하기 위해 해당 메서드는 인스턴스를 가리킨다는 의미로 인스턴스의 자기 참조 변수 this를 바인딩해줄 필요가 있는 것이다.
결론
이 문제를 해결하면서 es6 클래스, this의 의미, 실행 컨텍스트에 대하여 공부하게 되었다.
왜 면접 단골 질문으로 나올 만큼 중요한 지 알 것만 같다!
분명 학부에서 배운 클래스 기반 언어 C++, Java와는 다른 특이한 점들이 있지만 자바스크립트만의 매력이 있는 것 같다😃
비교적 사용하기에 쉬운 언어 같지만 공부하면 할수록 중요한 개념들이 많다고나 할까...
아무튼, 현재 사용하는 메서드의 컨텍스트를 잘 파악하고 this가 어디에 바인딩이 되어있는지 생각하면서 개발하자!💪🏼
전체 코드 확인
// Model.js
import React from "react";
import axios from "axios";
import Spinner from "react-bootstrap/Spinner";
import "./Model.css";
import { Analysis } from "../../pages";
class Model extends React.Component {
constructor(props) {
super(props);
this.state = {
inputFile: undefined,
infScore: undefined,
isLoading: false,
};
this.getModelScore = this.getModelScore.bind(this);
this.modelInference = this.modelInference.bind(this);
}
getModelScore = async () => {
const { inputFile } = this.state;
let formData = new FormData();
formData.append("data", inputFile);
try {
const response = await axios.post(
process.env.REACT_APP_SERVER + "/inference",
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
);
this.setState({ infScore: response.data });
} catch (err) {
console.log("Error 발생!!!: " + err);
}
};
modelInference() {
// model inference하고 결과 받아오기
const { inputFile } = this.state;
if (inputFile === undefined) {
alert("You forgot data!🤭"); // input없이 화면이 넘어오면 alert
} else {
this.setState({ isLoading: true }); // isLoading state를 true하며 로딩화면으로 렌더링
this.getModelScore(); // 모델 서버와 통신(서버 내에 raw 데이터 생성, 플롯을 response로 받는다.)
}
}
render() {
const { isLoading, infScore } = this.state;
console.log("rendering...."); // render function이 call된 것을 확인
// state가 변경되면 rendering이 다시 일어난다.
// csv file input을 받으면 button onclick 콜백함수로 modelInference가 실행되고
// isLoading state가 true가 되며 로딩화면이 뜬다.
if (isLoading === false) {
return (
<div className="file_upload">
<label className="file_label" htmlFor="file">
📂 Input file(csv) 📂
</label>
<input
id="file"
className="file_input"
type="file"
accept=".csv"
onChange={(event) => {
this.setState({
inputFile: event.target.files[0],
});
}}
/>
<button onClick={this.modelInference}>Start Inference🔎</button>
</div>
);
} else {
// modelInference 메서드에서 inputFile이 들어온 것을 체크해서 isLoading이 true가 된다면
// inference 결과를 받기 전까지 로딩화면을 띄운다.
if (infScore === undefined) {
return (
<div className="loading__container">
<Spinner
className="loading__logo"
animation="border"
variant="primary"
/>
</div>
);
} else {
// inference 결과로 받아온 모델 score와 플롯 두개를 props로 넘겨주고,
// analysis 컴포넌트에서 보여준다.
const {
accuracy_score,
roc_auc_score,
lgbm_plot_importance,
zero_one_distribution,
} = this.state.infScore;
return (
<Analysis
accuracy={accuracy_score}
auroc={roc_auc_score}
lgbm_plot={lgbm_plot_importance}
zero_one={zero_one_distribution}
/>
);
}
}
}
}
export default Model;
reference
stackoverflow - https://stackoverflow.com/questions/36309636/why-binding-is-needed-in-es6-react-classes
freecodecamp - https://www.freecodecamp.org/news/this-is-why-we-need-to-bind-event-handlers-in-class-components-in-react-f7ea1a6f93eb/
'Web_Frontend' 카테고리의 다른 글
[FE] img 태그 alt 속성 내용 (0) | 2022.01.28 |
---|---|
[FE] jsconfig.json 알아보기 (0) | 2022.01.06 |