약간의 기초지식이 있는 백엔드 개발자 입장에서, React.js를 처음 접하면서 느낀 점들을 정리해보고자 한다.
React를 바라보는 지식수준(현재 내 상태)
- Javascript의 기본적인 문법 지식이 있음
- node.js, express를 사용한 개발 경험이 있음
- html, css를 사용한 간단한 프론트 구현 경험이 있음
- React가 어떤 원리로 구동되는지, 어떤 컨셉인지는 전혀 모름
매우 유용했던 자료들
- 이고잉님의 생활코딩 React강의
- 리액트 공식 Docs
- velopert님의 누구든지 하는 리액트
이 글은 위 자료들을 보면서 인상깊었거나 중요하다고 생각되는 부분들을 정리한 글이니 참고해주세요.
제 경우 이고잉님의 강의 위주로 진행하면서, 중간중간 좀 더 알고 싶은 부분은 다른 자료를 참고해가며 공부했습니다.
먼저 리액트가 대략 어떤 느낌의 라이브러리인지, 왜 나오게 되었는지를 알아야 사용하는데 걸림돌이 없을 것 같아 간략히 알아보았다.
리액트의 컨셉
화면의 변화가 필요할 때, 어떤 요소를 어떻게 변경해주어야 하는가?에서 리액트는 '변경하지 말고 그냥 대체하자' 라는 컨셉으로 만들어졌다. 하지만 변화가 생길 때마다 전체 화면을 새로 계산한다면.. 성능상의 문제가 생긴다.
그래서 리액트는 virtual DOM이라는것을 사용해서, 가상 DOM으로 렌더링을 한 후 변화가 생긴 부분만 최소한으로 업데이트를 해주는 방법을 사용한다.
리액트의 핵심
html에 정의되어 있는 복잡한 태그들을 필요한만큼 묶어 사용자 정의 태그(=컴포넌트)로 분리할 수 있다.
이렇게 하면 :
- 가독성이 좋아지고
- 재사용이 가능해지고
- 문제가 되는 부분 컴포넌트만 수정하면 해당 컴포넌트가 포함된 모든 곳에서 수정된다(유지보수 용이)
새 리액트 프로젝트로 살펴보기
아주 간단하게 새 프로젝트를 시작할 수 있다.
$ npx create-react-app my-app
$ cd my-app
$ npm start
npm과 npx의 차이
npm이 앞으로도 계속 사용할 목적으로 다운로드 및 설치하는 것이라면, npx는 임시로 설치해서 사용 후 지우는 것이다.
React에서는 npx를 권장하는데, 이렇게 하면 늘 최신 버전을 사용하도록 유도할 수 있기 때문이다.
처음 create-react-app으로 생성하면, 아래와 같은 구조의 프로젝트가 생성된다.
주요 파일들과 내용을 살펴보자.
public/index.html
기본적인 태그들만 존재하는, 실제로 보여지는 내용은 없는 빈 화면이다.
<div id="root"></div>
리액트가 실행되면, root라는 div안에 리액트를 통해 만든 태그들이 들어가도록 세팅되어 있다.
src/index.js
ReactDOM.render(<App />, document.getElementById('root'));
App 컴포넌트를 위의 index.html의 root 안에 렌더링해서 넣겠다는 의미다.
함수의 인자로 html태그처럼 생긴 것을 사용하는데, 실제 html은 아니고 JSX문법이다.
html과 비슷하지만 정말로 html은 아니라는 점...
여기서 <App/>은 src/App.js에서 리턴된 App이라는 컴포넌트를 의미한다.
주의할 점은 render() { return() }의 return 안에는가장 바깥쪽에는 반드시 하나의 태그가 있어야 한다.
필요시 하나의 최상위 태그로 감싸주어야 한다.
<!-- 가능한 형태 -->
<div>hello</div>
<!-- 불가능한 형태 -->
<div>hello</div>
<div>world</div>
<!-- 가능한 형태 -->
<div>
<div>hello</div>
<div>world</div>
</div>
컴포넌트를 정의하는 방법은 함수 방식과 클래스 방식이 있다.
- const UserInfo = ({name}) ⇒ {}
- class UserInfo extends Component {}
함수 형태를 사용하면 state와 LifeCycle이 빠져있어서 컴포넌트 초기 마운트가 아주 미세하게 더 빠르다고 한다. 하지만 컴포넌트가 엄청나게 많은게 아니라면 큰 차이는 없다고 한다. 반대로 LifeCycle API를 사용할 수 없는 문제도 해결되었다고. 선호하는 방식을 사용하면 될 것 같다.
컴포넌트의 데이터를 변경하거나 전달하는 방법
동적 웹페이지를 만들기 위해서는 컴포넌트의 데이터들이 변경될 수 있어야 할 것이다.
리액트에서는 state와 props를 사용한다.
개인적으로 이 두가지가 굉장히 헷갈렸다.
props(=프로퍼티)는 태그 안에 가지고 있는 속성들, <input type="text" name="username" value="effy">
state는 컴포넌트마다 숨겨져 있는 상태값들, {id: 1, username: "effy"}
이런 느낌으로 받아들이고 나니 그나마 덜 헷갈렸다.
공통점 : 컴포넌트에서 사용할 수 있는 데이터다. 변경시 render가 일어난다.
props
- 사용자가 컴포넌트를 사용하는 입장에서 중요한 것.
- 부모 컴포넌트가 자식 컴포넌트에게 줌(외부에서 내부를 조작)
- 자식 컴포넌트가 props를 직접 수정할 수 없다
- 부모 컴포넌트 : <Hello name="effy" />
- 자식 컴포넌트 : {this.props.name} 으로 사용가능
- defaultProps
- 자식 컴포넌트 클래스 안에서 static defaultProps = {}로 선언해 사용가능
- 또는 Hello.defaultProps
- 함수형으로도 사용가능
state
- 자식 컴포넌트가 부모 컴포넌트에 데이터를 전달하고 싶을 때 사용
- 동적인 데이터를 다룰 때 사용
- 사용자에게 드러나서는 안되는 정보.
- props의 값 등에 따라 내부적인 구현에 필요한 데이터들
- {this.state.name}과 같은 형태로 보여줄 수 있다.
- this.setState로 변경해야 한다.
- 단순히 this.state.number++ 라고 해서는 리액트가 변경을 감지하지 못한다.
const { number } = this.state;
this.setState({
number: number + 1
});
이벤트
.bind(this)
메소드 작성시 this를 사용하는 경우, 이벤트가 발생하면 this가 끊기므로, 바인딩이 필요하다.
// 최초의 state는 생성자에서 설정해준다.
// constructor에서 .bind(this)해줘야함.
constructor(props) {
super(props);
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDecrease = this.handleDecrease.bind(this);
}
// 이미 컴포넌트 생성이 끝난 후 동적으로 state를 변경할 땐, this.setState를 사용한다.
// setState를 사용해야 리액트가 값이 변했다는 것을 알 수 있다.
handleIncrease = () => {
this.setState({
number: this.state.number + 1
});
}
// this를 사용하지 않으면 undefined가 된다.
var cat = {name: 'effy'}
function catCall() {
console.log(this.name);
}
catCall(); // 당연히 undefined
const catCall2 = catCall.bind(cat);
catCall2(); // effy
이벤트 설정시 유의사항
<button onClick={this.handleIncrease}>증가 버튼</button>
- 이벤트 이름은 반드시 camelCase로 작성
- html과 다름, onclick이라고 하면 안됨.
- 함수를 전달해야 한다. onClick={this.handleIncrease()} 이렇게 즉시 실행하는 형태로 전달하면 안됨!
- 렌더링 될때마다 함수가 호출되므로, 렌더링-함수호출-setState-렌더링... 무한반복됨
- event 안에서 this는 undefined이므로 state 등을 사용하고 싶을 땐 함수.bind(this)를 해주어야 컴포넌트의 this를 주입할 수 있다.
컴포넌트 이벤트 만들기
// class App
...
<ATag onChangePage={ // onChangePage라는 props로 함수를 전달한다.
function(){
alert('hi');
this.setState({
name: 'effy'
});
}
}.bind(this)
/>
...
// class ATag
...
<a onClick={ // onClick이벤트 발생시 props로 전달받은 함수를 실행시킨다.
function(e){
e.preventDefault(); // a태그가 기본적으로 가지고 있는 이벤트는 실행되지 않게 한다.
this.props.onChangePage();
}
}>click me!</a>
- onClick등의 이벤트 안에 작성된 익명함수는 기본 인자로 이벤트 자체가 들어온다.(위 코드의 e)
- onClick, onChange, onChangePage, onChangeMode, onSubmit등이 있다.
- e.preventDefault : 해당 태그의 기본적인 이벤트를 막는다.
- a태그 ⇒ 페이지 이동
- submit ⇒ 새로고침 (submit 자체는 작동함) 등
- 이벤트 내부에는 target이라는 속성이 있어서 해당 이벤트가 위치한 태그와 속성에 접근할 수 있다. (ex. e.target.name)
- data- 로 시작하는 속성은 dataset이라는 특수한 이름으로 접근할 수 있다.
- 예를 들어 위 코드에서, data-id에 접근하고 싶다면 e.target.dataset.id로 접근할 수 있다.
- 또한 input 태그의 값에는 태그의 name으로도 접근할 수 있다.
- <input type="text" name="title"/> // e.target.title.value
- 또는, 인자로 바인딩해줄수도 있다. 이 경우 함수의 첫번째 인자부터 차례로 들어가게 된다.(항상 이벤트가 가장 마지막 인자가 된다.)
- 아래 예시처럼...
<a data-id=123 onClick={
function(e) {
e.target // a 태그
e.target.dataset.id // data-id
}.bind(this);
}>click me!</a>
shouldComponentUpdate함수
- 기본적으로 변경이 일어난 컴포넌트와 모든 자식 컴포넌트는 render가 일어난다.
- 그럴때 shoudComponentUpdate를 사용하면 된다.
- true가 리턴되는 경우에만 render()를 실행시키도록 해준다.
// class Content
shouldComponentUpdate(newProps, newState){
// newProps는 변경사항이 생긴 이후의 props다.
console.log(newProps.data, this.props.data);
// 처음에는 두 가지가 같지만, 변경사항이 있으면 달라진다.
return this.props.data !== newProps.data;
}
- 하지만 concat이 아닌 데이터 원본을 변경하는 경우, newProps와 this.props가 같게 됨. (newProps === this.props)
- 그러므로 shouldComponentUpdate 메서드를 사용하고싶다면 원본 변경을 하지말고 복제본을 사용하는 것이 권장됨
- 예를들면 리스트에서는 오리지널 데이터를 수정하는 push가 아닌 concat 또는 Array.from()으로 복제 후 새로 할당해서 사용하는 등
- object는 Object.assign() 사용
- 이러한 문제에서 벗어나고 싶다면 immutable.js등을 사용하면 좋다.
Update
CRUD 중 Update에서 유의할 사항이 많아 따로 정리한다.
- 기존 데이터를 props로 받아와서 input value로 넣어주면 안된다. 리액트 자체에서 prop을 직접 수정하는 것을 방지하고 있기 때문이다.
- 그러므로 state화 시켜주어야 한다.
- 하지만, state를 대입하더라도, 변경사항은 감지되고 있지 않기 때문에 리액트에서 수정을 막는다.
- 그러므로.. onChange와 함께 사용해야 한다.
constructor(props) {
super(props);
this.state = {
title: this.props.data.title
}
}
...
// render
<input type="text" name="title"
value={this.state.title}
onChange={function(e){
this.setState({title: e.target.value});
}.bind(this);}
></input>
추가적으로,
여러 input tag들의 값을 받아와 setState해주는 함수를 분리하면 좋다.
constructor(props){
...
// 생성자에서 this를 바인딩해주면 태그마다 다시 해줄 필요가 없다.
this.inputFormHandler = this.inputFormHandler.bind(this);
}
inputFormHandler(e){
this.setState({[e.target.name]: e.target.value})
}
...
// render
<input type="text" name="title"
value={this.state.title}
onChange={this.inputFormHandler}
></input>
자잘한 팁
빌드하기
npm run build
- 빌드를 실행한다.
npx serve -s build
- 빌드하고 빌드한 프로젝트를 띄우기
inline 조건문
if(condition) { // welcome 메시지 띄우기} 같은 경우
{condition && <span>{this.props.name}님 안녕하세요</span>} 형태로 사용
debugger; 활용
코드에 debugger 를 포함시키면
크롬 개발자도구를 켜놓았을 때 해당 코드를 만나면 디버그 모드가 켜진다.
댓글