웹 페이지를 만들 때 HTML, CSS, JS로 DOM조작을 하는 방법과 프론트엔드 라이브러리(or Framework)인 리액트(React)를 활용하는 방법이 있다. 웹 페이지는 단순히 데이터만을 보여주는 공간이 아니라 사용자와 수많은 상호작용이 일어나는 공간인데 이를 위해선 그만큼 많은 상태 관리가 필요하다. 예를 들어 배경 이미지를 바꾸는 버튼을 눌렀을 때 1)바꿀 이미지 DOM을 찾고, 2)이미지 DOM의 소스를 바꿀 이미지로 변경하고, 3)바뀐 이미지를 다시 화면에 띄워주는 과정을 거쳐야 한다. 그러나 이 방법은 관리해야 할 DOM이 많아질수록 매우 복잡하고 어렵다. 따라서 DOM 관리와 상태 관리를 최소화하고 오직 기능 개발에만 집중할 수 있도록 만들어진 것이 바로 React이다.
리액트는 컴포넌트(Component)개념에 집중하고 있는데 컴포넌트는 하나의 의미를 가진 독립적인 단위 모듈로, 나만의 HTML태그라고 할 수 있다. 따라서 컴포넌트를 활용하면 코드가 직관적이고 재사용성이 높아진다.
<Tweet userId="walli" time="43">
hello, my name is walli :)
</Tweet>
props는 속성을 나타내는 데이터로 파라미터, 입력값(input)이라고 보면 된다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
ES6문법을 적용하여 작성할 수도 있다.
function Welcome({name}) {
return <h1>Hello, {name}</h1>;
}
리액트는 기본적으로 ES6를 사용하기 때문에 기본적인 ES6 개념들을 숙지해야 한다(Destructuring, Spread operator, Rest parameters, Default parameters, Template literals, arrow function, for-of loop 등)
JSX는 자바스크립트의 확장 문법으로 리액트 컴포넌트를 화면에 보여주기 위해 사용한다. 리액트 컴포넌트에서는 아래와 같이 반드시 JSX를 리턴해줘야 한다.
class Hello extends Component{
render(){
return(
<div>
<h1>hello, world</h1>
</div>
)
}
}
function Hello(){
return(
<div>
<h1>hello, world</h1>
</div>
)
}
JSX를 활용하면 간단하게 태그를 작성할 수 있고 가독성을 높일 수 있다. JSX문법으로 코드를 작성하면 Babel이라는 자바스크립트 컴파일러가 JSX를 자바스크립트로 변환해준다. 단, JSX문법을 사용할 때 지켜야 할 몇가지 규칙이 있다.
1) 반드시 하나의 엘리먼트로 감싸야 한다. 즉 엘리먼트가 여러개일 경우 최상위 엘리먼트 하나로 모든 엘리먼트를 감싸줘야 한다.
2) 자바스크립트 코드를 적용할 땐 중괄호{} 안에 작성한다.class App extends Component{ render(){ const name = 'walli'; return( <div> hello {name}! </div> ); } }
3) JSX 내부에서는 if문을 사용할 수 없으며, 삼항연산자를 사용해야 한다.
<div> { (1 + 1 === 2)?(<h1>정답</h1>):(<h1>탈락</h1>) } </div>
4) 엘리먼트의 클래스 이름을 적용할 때 class가 아닌 className을 사용해야 한다.
State는 살면서 변할 수 있는 값을 의미한다. 다시 말해, 컴포넌트 사용 중 컴포넌트 내부에서 변할 수 있는 값을 말한다.
props: 외부로부터 전달받은 값(위에서 넘겨받음)
state: 컴포넌트 내부에서 변화하는 값
State를 가진 컴포넌트는 함수 컴포넌트가 아닌 클래스 컴포넌트로 만들어야 한다.
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
클래스에 초기 this.state를 지정하려면 class constructor를 추가하면 된다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
이벤트를 처리하는 방법은 다음과 같다.
<button onClick={activateLasers}>
Activate Lasers
</button>
State를 변경할 때는 this.setState를 사용해야 하며, 이벤트 처리 시 바인딩을 해줘야 한다.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 콜백에서 `this`가 작동하려면 아래와 같이 바인딩 해주어야 합니다.
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
컴포넌트에 있어서 중요한 순간은?
(생성)
생성될 때
화면에 등장한 후
(업데이트)
새로운 props를 받을 때
새로운 상태를 가질 때
새로운 상태를 갖고 난 후
(제거)
화면에서 사라지기(unmount) 전
위와 같이 매 중요한 순간마다 컴포넌트는 새로 렌더링(render)된다(사라지기 전은 제외). 이를 다음 그림과 같이 나타낼 수 있다.
이미지 출처: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
컴포넌트 클래스에서 Lifecycle메소드를 선언하여 컴포넌트가 마운트되거나 언마운트 될 때 일부 코드를 작동할 수 있다. Lifecycle메소드는 State와 밀접한 관련이 있기 때문에 클래스 컴포넌트에서만 사용할 수 있고 함수 컴포넌트에서는 사용할 수 없다.
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 콜백에서 `this`가 작동하려면 아래와 같이 바인딩 해주어야 합니다.
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
//생성
componentDidMount() {
console.log('화면에 등장한 후')
}
//업데이트
componentDidUpdate() {
console.log('새로운 상태를 갖고 난 후')
}
//제거
componentWillUnmount() {
console.log('화면에서 사라지기 전')
}
render() {
return (
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
}
State가 변화되었을 때, 혹은 생성되었을 때, 컴포넌트 안에서 비동기 요청을 보낼 때 render나 constructor에서 보낼 수 없기 때문에(constructor, render는 async/await으로 사용할 수 없음), 그 때 Lifecycle메소드를 사용해서 비동기 요청을 보낼 수 있다.
컴포넌트는 컴포넌트 외부에서 props를 이용해 데이터를 마치 인자(arguments) 혹은 속성(attributes)처럼 전달받을 수 있다. 즉 데이터를 전달하는 주체는 부모 컴포넌트가 된다. 이는 데이터 흐름이 하향식(top-down)임을 의미한다.이 원칙은 단방향 데이터 흐름(one-way data flow)이라는 키워드가 React를 대표하는 설명 중 하나일 정도로 매우 중요하다. 또한 컴포넌트는 props를 통해 전달받은 데이터가 어디서 왔는지 전혀 알지 못한다.
<데이터의 State(상태) 판단 기준>
부모로부터 props를 통해 전달되면 => state가 아님
시간이 지나도 변하지 않는다면 => state가 아님
컴포넌트 안의 다른 state나 props를 가지고 계산 가능하면 => state가 아님
어떤 데이터를 State(상태)로 두고 위치를 정하고 나면, 부모 컴포넌트의 상태가 하위 컴포넌트에 의해 변하는 경우(역방향 데이터 흐름)가 있다. 예를 들어, twittler 에서 새 글을 추가하는 이벤트가 발생할 때 전체 트윗 목록에 새로운 트윗 객체가 추가되는 경우가 이에 해당된다. 하위 컴포넌트에서의 클릭 이벤트가 부모의 상태를 변화시켜야 하는 것이다. 이를 해결할 수 있는 방법이 바로 "State 끌어올리기(Lifting state up)"이다.
State 끌어올리기(Lifting state up): 상위 컴포넌트의 "상태를 변경하는 함수" 그 자체를 하위 컴포넌트로 전달하고, 이 함수를 하위 컴포넌트가 실행하는 것(상위에 있는 state 값을 하위에서 수정하여 상위로 올려주는 것)
상태를 변경하는 함수는 handleChangeValue이다. 전달은 props(handleButtonClick)를 이용한다.
class ParentComponent extends React.Component {
// ...생략...
//상태 변경 함수
handleChangeValue() {
this.setState({
value: '보여줄게 완전히 달라진 값'
})
}
render() {
return <div>
<div>값은 {this.state.value} 입니다</div>
<ChildComponent handleButtonClick={this.handleChangeValue} />
</div>
}
}
ChildComponent는 props로 전달받은 함수를 컴포넌트 내에서 실행할 수 있게 된다. "상태 변경 함수"는 버튼이 클릭할 때 실행되기를 원하므로, 해당 부분에 콜백 함수를 실행한다.
function ChildComponent({ handleButtonClick }) {
const handleClick = () => {
// Q. 이 버튼을 눌러서 부모의 상태를 바꿀 순 없을까?
// A. 인자로 받은 상태 변경 함수를 실행하자!
handleButtonClick()
}
return (
<button onClick={handleClick}>값 변경</button>
)
}
필요에 따라 설정할 값을 콜백 함수의 인자로 넘길 수도 있다.
class ParentComponent extends React.Component {
// ...생략...
handleChangeValue(newValue) {
this.setState({
value: newValue
})
}
// ...생략...
}
function ChildComponent({ handleButtonClick }) {
const handleClick = () => {
handleButtonClick('넘겨줄게 자식이 원하는 값')
}
return (
<button onClick={handleClick}>값 변경</button>
)
}