[TIL] React #3

39999 단어 TILReactReact

공부할 책 : Doit! 리액트 프로그래밍 정석

앞에서 프로퍼티를 공부했다. 여름방학때 분명 다뤘는데도 조금 어렵쓰..
복습해둬야겠다...!

이번엔 컴포넌트의 상태를 저장하고 변경할 수 있는 데이터인 state와
기타 등등을 공부할 예정!


컴포넌트 상태 관리하기

state는 '값을 저장하거나 변경할 수 있는 객체'로 보통 버튼을 클릭하거나 값을 입력하는 등의 이벤트와 함께 사용된다. 예를 들어 어떤 버튼을 눌렀을 때 버튼 색을 변경하거나 글씨 모양을 바꿀 때 사용된다. 이제부터 state 의 사용 방법을 자세히 알아보자. 다음은 setTimeout() 함수를 통해 4초 후 state에 저장되어 있는 값을 변경하는 예제이다.

StateExample.jsx

import React from 'react';

class StateExample extends React.Component {
    constructor(props){
        super(props);
        //state 정의
        this.state = {
            loading:true,
            formData:'no data',
        };

        this.handleData=this.handleData.bind(this);
        setTimeout(this.handleData,4000);
    }
    handleData(){
        const data = 'new data';
        const {formData} = this.state;
        this.setState({
            loading:false,
            formData: data+formData,
        });
        console.log('loading값',this.state.loading);
    }
    render(){
        return(
            <div>
                <span>로딩중 : {String(this.state.loading)}</span>
                <span>결과 : {this.state.formData}</span>
            </div>
        )
    }
}
export default StateExample

App.js

import React from 'react';
import StateExample from './03/StateExample';

class App extends React.Component{
  render(){
    return (
      <div>
        <div>
          <StateExample/>
        </div>
      </div>
    )
  }
}
export default App;

결과를 보면 화면이 4초 뒤에 바뀐다. 프로퍼티에서 단순히 값을 읽어 화면에 출력하는 것과 비교해 보면 state의 유용함이 잘 느껴질 것이다.

그러나 아래의 사항을 주의해야 한다.

state를 사용할 때 주의할 점
1. 생성자(constructor)에서 반드시 초기화해야 한다.
2. state 값을 변경할 때는 setState()함수를 반드시 사용해야 한다.
3. setState()함수는 비동기로 처리되며, setState() 코드 이후로 연결된 함수들의 실행이 완료된 시점에 화면 동기화 과정을 거친다.

state객체는 반드시 초기화해야 한다. 그렇지 않으면 내부 함수에서 state값에 접근할 수 없다. 만약 마땅한 초기값이 없다면 state에 빈 객체라도 넣어야 한다. 그리고 state에 저장되는 객체의 값은 직접 변경하면 안 된다.

state값은 setState() 함수로 변경한다.
state값을 직접 변경하면 안 되는 이유는 render() 함수로 화면을 그려주는 시점은 리액트 엔진이 정하기 때문이다. 즉 state값을 직접 변경해도 render() 함수는 새로 호출되지 않는다. 하지만 setState() 함수를 호출하여 state값을 변경하면 리액트 엔진이 자동으로 render() 함수를 호출하므로 화면에 변경된 state값을 새롭게 출력할 수 있다.
SetState() 함수는 다음에 배울 컴포넌트의 생명주기와 깊이 연관되어 있다. 실제로 리액트 엔진은 setState() 함수로 state값을 변경하면 몇 단계의 검증 과정을 거쳐 render() 함수를 호출한다.

setState 함수의 인자로 함수를 전달하면 이전 state값을 쉽게 읽을 수 있다.
setState() 함수의 인자로 함수를 전달하면 이전 state값을 따로 읽는 과정을 생략할 수 있다. 다음은 일반 함수와 화살표 함수를 setState() 함수의 인자로 전달하여 state값을 변경한 것이다. 이 방법은 이후에도 종종 사용하므로 미리 익혀두자!

//일반 함수를 사용한 예
handleData(data){
    this.setState(function(prevState) {
        const newState = {
            loading : false,
            formData : data + prevState.formData,
        }
        return newState;
    });
}

// 화살표 함수를 사용한 예
handleData(Data) {
    this.setState(prevState => ({
        loading : false,
        formData : data + prevState.formData
    });
}

forceUpdate() 함수로 state 관리하기
꼭 setState() 함수로 state를 관리할 필요는 없다. setState() 함수를 사용하는 이유는 앞에서 언급했던 것처럼 리액트 엔진이 state의 변경과 화면 동기화 과정을 처리해야 하기 때문이다. 만약 출력 검증 작업 없이 함수가 호출될 때마다 새롭게 화면을 출력하고 싶다면 클래스 인스턴스 변수와 화면을 강제로 출력해주는 forceUpdate() 함수를 사용하면 된다. 클래스 인스턴스 변수와 forceUpdate() 함수의 사용 방법은 아래와 같다.

단. 이 방법은 리액트 성능에 제약이 있으므로 매번 새롭게 화면을 출력해야 하는 경우가 아니라면 가급적 사용하지 말자.

ForceUpdate.jsx

import React from 'react';

class ForceUpdateExample extends React.Component{
    constructor(props){
        super(props);
        this.loading = true;
        this.formData = 'no data';
        this.handleData = this.handleData.bind(this);
        setTimeout(this.handleData,4000);
    }
    handleData(){
        const data ='new data';
        this.loading = false;
        this.formData = data + this.formData;
        this.forceUpdate();
    }
    render(){
        return(
            <div>
                <span>로딩중 : {String(this.loading)}</span>
                <span>결과 : {this.formData}</span>
            </div>
        )
    }
}

export default ForceUpdateExample;

App.jsx

import React from 'react';
import ForceUpdateExample from './03/ForceUpdateExample';

class App extends React.Component{
  render(){
    return (
      <div>
        <div>
          <ForceUpdateExample/>
        </div>
      </div>
    )
  }
}
export default App;

setState 를 사용하는 예제 :

import React from 'react';

class Counter extends React.Component{
    constructor(props){
        super(props);
        this.state={
            count:0
        };
        this.increaseCount = this.increaseCount.bind(this);
    }
    increaseCount(){
        const {count} = this.state;
        this.setState({count:count+1})
    }
    render(){
        return(
            <div>
                <span>카운트 : {this.state.count}</span>
                <button onClick={this.increaseCount}>카운트 증가</button>
            </div>
        )
    }
}
export default Counter;

컴포넌트의 생명주기

컴포넌트의 생성부터 소멸까지의 과정을 컴포넌트의 생명주기라고 부른다. 컴포넌트는 생명주기마다 함수를 가지고 있는데 이 함수들을 이용하면 특정 시점에 원하는 동작을 하도록 만들 수 있다. 컴포넌트의 생명주기는 여러 번 보면서 익히는 것이 중요하다.

생명주기 함수

생명주기 함수는 render() 함수를 포함해 총 8종의 함수가 있다. 생명주기 함수는 리액트 엔진에서 자동으로 호출한다. 즉 개발자가 마음대로 호출할 수 없다.

constructor(props) 함수

맨 처음에 생성될 때 한 번만 호출되며, 상태를 선언할 때 사용된다. constructor() 함수를 정의할 때는 항상 super() 함수를 가장 위에 호출해야 한다. super() 함수에는 프로퍼티와 생명 주기 상태 등을 초기화하는 중요한 과정을 포함하고 있기 때문이다.

render() 함수

render() 함수는 데이터가 변경되어 새 화면을 그려야 할 때 자동으로 호출되는 함수이다.

getDerivedStateFromProps(prop, state) 함수

이 함수는 정적 함수이다. 따라서 함수 안에서 this.prop이나 this.state와 같은 방법으로 프로퍼티나 state값에 접근할 수 없다. 만약 각 값에 접근해야 하는 경우 반드시 인자로 전달된 props, state를 이용해야 한다. 이때 props는 상위 컴포넌트에서 전달된 값이며, state는 현재 컴포넌트의 state값이다. 이 함수는 상위 컴포넌트에서 전달받은 프로퍼티로 state값을 연동할 때 주로 사용되며, 반환값으로 state를 변경한다.

componentDidMount() 함수

componentDidMount() 함수는 render() 함수가 JSX를 화면에 그린 이후에 호출되는 함수이다. 만약 컴포넌트가 화면에 모두 표현된 이후 해야 하는 작업은 여기서 하면 된다.

shouldComponentUpdate(nextProps,nextState) 함수

shouldComponentUpdate() 함수는 프로퍼티를 변경하거나 setState() 함수를 호출하여 state값을 변경하면 '화면을 새로 출력해야 하는지' 판단하는 함수이다. 이 함수는 화면을 새로 출력할지 말지 판단하며, 데이터 변화를 비교하는 작업을 포함하므로 리액트 성능에 영향을 많이 준다. 화면 변경을 위해 검증 작업을 해야 하는 경우 이 함수를 사용하면 된다. forceUpdate()함수를 호출하여 화면을 출력하면 이 함수는 호출되지 않는다.

getSnapshotBeforeUpdate(prevProps, prevState) 함수

getSnapshotBeforeUpdate() 함수는 컴포넌트의 변경된 내용이 가상 화면에 완성된 이후 호출되는 함수이다. 이 함수는 컴포넌트가 화면에 실제로 출력되기 전에 호출되므로 화면에 출력될 엘리먼트의 크기 또는 스크롤 위치 등의 DOM 정보에 접근할 때 사용된다.

componentDidUpdate(prevProps,prevState,snapshot) 함수

componentDidUpdate() 함수는 컴포넌트가 실제 화면에 출력된 이후 호출되는 함수이다. 이 함수는 부모 컴포넌트로부터 전달된 이전 프로퍼티와 이전 state값과 함께 getSnapShotBeforeUpdate() 함수에서 반환된 값을 인자로 전달받는다. 이 값들을 이용하여 스크롤 위치를 옮기거나 커서를 이동시키는 등의 DOM 정보를 변경할 때 사용한다.

componentWillUnmount() 함수

componentWillUnmount() 함수는 컴포넌트가 소멸되기 직전에 호출되는 함수이다. 보통 컴포넌트에서 감시하고 있는 작업들을 해제할 때 필요한 함수이다. 예를 들어 컴포넌트에 setInterval() 함수가 사용되었다면 이 함수에서 setInterval() 함수를 clearInterval() 함수로 해제해야 한다. 이러한 해제 작업이 생략되면 메모리 누수 현상이 발생하여 웹 브라우저의 작동이 멈추기도 한다.

생성 생명주기 실행 과정

변경 생명주기 실행 과정

소멸 생성주기 실행 과정

카운터 프로그램

부모 컴포넌트로부터 count의 초깃값을 전달받아 숫자를 증가하는 카운터 프로그램이다.
getDerivedStateFromProps() 함수의 동작 원리도 같이 알아보자.

Counter.jsx

import React from 'react';

class Counter extends React.Component {
  constructor(props){
    super(props);
    this.state={
      count:props.count,
    };
    this.increaseCount = this.increaseCount.bind(this);
  }
  increaseCount(){
    this.setState(({count}) => ({
      count:count+1
    }))
  }
  render(){
    return(
      <div>
        현재 카운트 : {this.state.count}
        <button onClick={this.increaseCount}>카운트 증가</button>
      </div>
    )
  }
}
export default Counter;

NewCounter.jsx

import React from 'react';

class NewCounter extends React.Component {
    constructor(props){
        super(props);
        this.state = {};
        this.increaseCount = this.increaseCount.bind(this);
    }

    static getDerivedStateFromProps(props, state){
        const {count} = props;
        return {
            count,
            newCount:count === state.count ? state.newCount : count
            // 프로퍼티가 변경되었다면 변경된 프로퍼티값으로, 변경되지 않았다면 기존 State 값으로 설정
        }
    }
    increaseCount(){
        this.setState(({newCount}) => ({
            newCount:newCount+1
        }))
    }
    render(){
        return(
            <div>
                현재 카운트 : {this.state.newCount}
                <button onClick={this.increaseCount}>카운트 증가</button>
            </div>
        )
    }
}
export default NewCounter;

App 컴포넌트가 전달한 최초의 프로퍼티값은 state.count에 저장되며, NewCounter 컴포넌트는 state.newCount로 증가값을 따로 분리하여 관리한다. state.count가 아니라 state.newCount로 증가값을 관리하는 이유는 getDerivedStateFromProps() 함수는 다른 프로퍼티가 변경되어도 호출되기 때문이다. count 프로퍼티가 변경되었는지 비교하려면 위의 코드처럼 값을 따로 관리해야 한다.

App.jsx

import React from 'react';
import Counter from './03/Counter';
import NewCounter from './03/NewCounter';

class App extends React.Component{
  constructor(props){
    super(props);
    this.state = {count:10};
    this.resetCount = this.resetCount.bind(this);
  }
  resetCount(){
    this.setState(({count}) => ({count:count +10}));
  }

  render(){
    return (
      <div>
        <div>
          <Counter count={this.state.count}/>
        </div>
        <div>
          <NewCounter count={this.state.count}/>
        </div>
        <button onClick={this.resetCount}>{this.state.count+10}으로 초기화</button>
      </div>
    )
  }
}
export default App;

실행 결과 :

NewCounter 컴포넌트만 getDerivedStateFromProps() 함수로 App 컴포넌트부터 갱신된 프로퍼티값을 동기화 했기 때문이다. Counter 컴포넌트는 처음 생성될 때만 프로퍼티값으로 state 값을 설정하므로 갱신 과정에서는 state 값이 변경되지 않는다.

생명주기 함수의 실행 과정을 잘 이해하자!

클래스형 컴포넌트

클래스형 컴포넌트는 생명주기 함수와 컴포넌트 구성 요소를 모두 포함하고 있다.
클래스형 컴포넌트는 두 종류의 클래스(Component, PureComponent)를 사용하는데, 여기서는 이 두 클래스의 차이점과 장단점을 알아보자.

Component

Component 클래스는 프로퍼티, state와 생명주기 함수가 들어있는 구조의 컴포넌트를 만들 때 사용한다. 이 클래스는 지금까지 컴포넌트를 만들 때 많이 사용했다.
그래서 패스

PureComponent

PureComponent 클래스는 Component 클래스를 상속받은 클래스이다. PureComponent 클래스는 shouldComponentUpdate() 함수를 '얕은 비교'를 하도록 재정의했다. PureComponent 클래스로 만들어진 컴포넌트는 '얕은 비교를 통해 데이터가 변경된 경우에만 render() 함수를 호출한다. 반면 Component 클래스로 만들어진 컴포넌트는 항상 render() 함수를 호출한다.

얕은 비교가 무엇인지 잠시 살펴보자.

import shallowEqual from 'shallow-equal';
const obj = { name: 'park' };
const mylist = [1, 2, 3, obj];
const list1 = [1, 2, 3, obj];
const list2 = [1, 2, 3, { name: 'park' }];

첫 번째로 깊은 비교를 하는 === 이다.

mylist === list1 // false

mylist와 list1은 같은 요소를 가지고 있지만 각각 새롭게 정의된 배열이므로 false이다. 그렇다고 배열 하나하나를 모두 비교해 볼 수는 없다. 바로 이런 경우 얕은 비교를 해 주는 shallowEqual() 함수를 사용한다. shallowEqual() 함수로 mylist와 list1을 비교하면 true를 반환한다.

shallowEqual(mylist, list1) // true

그런데 list1과 list2 를 넣어보면 false가 나온다.
list2의 마지막 요소는 obj와 같은 요소를 포함하고 있지만 새 객체이기 때문이다.

shallowEqual(list1, list2) // false

이렇게 내용물을 모두 비교하지 않는 이유는 비교 검사 작업이 성능에 영향을 끼치기 때문이다. PureComponent 클래스로 선언한 컴포넌트에서는 shouldComponentUpdate() 함수에서 shallowEqual() 함수로 얕은 비교를 하여 데이터의 변경이 있으면 화면을 새로 출력하고 Component 클래스로 선언한 컴포넌트는 이러한 비교 과정 없이 항상 새로 출력한다는 것을 기억하자.

PureComponent와 불변 변수로 성능을 높인다.
이 책에서는 Component 대신 얕은 비교를 사용하는 PureComponent로 컴포넌트를 만들어 리액트 앱의 성능을 높인다. 이때 앱의 성능을 최대화하기 위해 불변 변수도 사용한다. 불변 변수를 활용해 정보가 바뀔 때마다 새 배열을 할당하는 방식으로 비교 속도를 최적화할 수 있다.

함수형 컴포넌트

함수형 컴포넌트는 조금 길게 표현하여 state가 없는 함수형 컴포넌트라고 부른다. 이를 줄여 SFC라고 부르기도 한다. 이름에서 알 수 있듯 함수형 컴포넌트는 state를 포함하지 않으며 데이터를 받아 출력할 컴포넌트를 반환한다. 함수형 컴포넌트에 대해 자세히 알아보자.

함수형 컴포넌트는 함수와 동일한 구조를 가지고 있다. 입력받은 프로퍼티와 컨텍스트를 이용하여 화면을 출력하는 것이다. 다음 그림을 떠올리면 이해하기 쉽다.

예제 :

import React from 'react';
import PropTypes from 'prop-types';

function SFC(props, context){
    const {somePropValue}=props;
    const {someContextValue}=context;
    return <h1>Hello, {somePropValue}</h1>
}

SFC.propTypes = {somePropValue:PropTypes.any};
SFC.defaultProps = {somePropValue:'default value'};

export default SFC;

함수형 컴포넌트에는 클래스 선언이 없다.상위 컴포넌트로부터 전달받은 프로퍼티와 컨텍스트만을 이용하여 화면을 구성한다. 그리고 함수형 컴포넌트는 state와 수명주기 함수를 사용할 수 없다.
그래서 함수형 컴포넌트는 단순한 구조의 UI를 제작할 때 많이 사용된다.

아래는 TodaysPlanApp 컴포넌트를 클래스형 컴포넌트와 함수형 컴포넌트를 작성한 코드이다.

//클래스형 컴포넌트
class TodaysPlanApp extends React.Component {
    render(){
        const {onButtonClick,hasPlan} = this.props;
        return (
            <div className="body">
                {hasPlan ? <TodaysPlan/> : null}
                <button onClick={onButtonClick}>
                    계획없음
                </button>
            </div>
        )
    }
}

//함수형 컴포넌트
function TodaysPlanApp(props){
    const {onButtonClick,hasPlan}=props;
    return (
        <div className="body">
            {hasPlan ? <TodaysPlan/>:null}
            <button onClick={onButtonClick}>
                계획없음
            </button>
        </div>
    )
}

배열 컴포넌트

게시판이나 유튜브 영상 목록은 어떻게 구현될까? 바로 자바스크립트의 배열을 사용한다.
자바스크립트 배열은 아래처럼 다양한 자료형을 저장할 수 있다.

const mixedList = [1, 'str', {}, function a() {}];

이 성질을 이용해 XML와 JSX도 배열에 저장할 수 있다.

const componentList = [<MyComponent/>,<MySecondComponent/>,<b>Hi</b>];

map() 함수를 사용하면 배열로 저장된 데이터를 바로 JSX로 변경할 수 있다. 다음은 todoList에 저장된 객체를 map() 함수를 통해 JSX로 변경한 것이다.

const todoList = [
{ taskName:'빨래하기', finished:false },
{ taskName:'공부하기', finished:true },
];
const todos = todoList.map(todo=><div>{todo.taskName}</div>);
//결과
//[<div>빨래하기</div>, <div>공부하기</div>]

배열 컴포넌트의 경우 배열 요소의 개수만큼 반복하므로 성능에 영향을 많이 준다. 배열 컴포넌트에는 키값을 key로 꼭 정의해 주어야 한다. 키값을 정의하여 출력한 배열 컴포넌트는 다시 출력해야 하는 경우 리액트 엔진이 기존의 컴포넌트를 재활용하여 성능을 높일 수 있기 때문이다. 다음은 키값을 정의한 배열 컴포넌트의 활용이다.

...
render(){
  const todoLIst = [
    { taskName: '빨래하기', finished: false },
    { taskName: '공부하기', finished: true },
  ];
  return (
    <div>
      {todoLIst.map((todo,i)=> <div key={`tl_${i}`}>{todo.taskName}</div>)}
    </div>
  )
}
...

그러나 filter() 함수를 추가하여 배열 항목에서 finished 값이 false인 항목을 제외하는 등의 항목을 변형하는 경우 인덱스 번호를 키로 사용한다면 키값이 함께 변경되는 문제가 발생한다.
이러한 문제를 해결하려면 키값으로 고유한 배열 항목을 사용하면 된다. 위 예제는 taskName에 고유한 값이 들어 있으므로 이를 키값으로 사용하면 이전 키값과 충돌하는 문제를 해결할 수 있다.

import React from 'react';

class TodoList extends React.PureComponent {
  render(){
    const todoList=[
      { taskName:'빨래하기',finished:false },
      { taskName:'공부하기',finished:true }, 
    ];
    return (
      <div>
        {todoList.map((todo)=> <div key={todo.taskName}>{todo.taskName}</div>)}
      </div>
    )
  }
}

export default TodoList;

map 함수로 이런것도 가능하다

import React from 'react';

class TodoList extends React.PureComponent {
  render(){
    return [1,2,3].map((num)=>(
      <input type="radio" mame={`option${num}`} key={`${num}`} value={`${num}`} label={`${num}개`}/> 
    ))
  }
}

export default TodoList;

컴포넌트에서 콜백 함수와 이벤트 처리하기

앞에서 프로퍼티를 사용하면 상위 컴포넌트의 데이터를 하위 컴포넌트에 전달할 수 있다는 것을 배웠다. 만약 하위 컴포넌트에서 프로퍼티를 변경해야 할 때는 어떻게 해야 할까?

프로퍼티 원본을 수정할 수 있는 함수를 하위 컴포넌트에 제공하면 된다. 콜백 함수란 정의된 위치에서 실행되지 않고, 이후 특정 상황에서 실행되는 함수를 말한다. 즉 콜백 함수를 프로퍼티로 전달하면 된다.

다음과 같이 increaseCount() 함수를 프로퍼티 형태로 전달하면 count 값을 변경할 수 있다. Counter 컴포넌트의 프로퍼티로 onAdd를 추가한다.

App.js

import React from 'react';
import Counter from './03/Counter2';

class App extends React.Component{
  constructor(props){
    super(props);
    this.state={
      count:1,
    };
  }
  increaseCount(){
    this.setState(({count})=>({count:count+1}))
  }
  render(){
    return (
      <Counter count={this.state.count} onAdd={this.increaseCount}/>
    )
  }
}
export default App;

Counter2.jsx

import React from 'react';
import PropTypes from 'prop-types';

class Counter2 extends React.Component{
    render(){
        return(
            <div>
                현재 카운트 : {this.props.count};
                <button onClick={()=>this.props.onAdd()}>
                    카운트 증가
                </button>
            </div>
        )
    }
}

Counter.propTypes={
    count:PropTypes.number,
    onAdd:PropTypes.func
}

export default Counter2;

bind() 함수로 this 범위 오류 해결하기

그러나 카운트 증가 버튼을 눌러도 제대로 작동하지 않는다. 오류 메시지를 보면 다음과 같이 출력된다.
Uncaught TypeError: this.setState is not a function
Counter2 컴포넌트에서 프로퍼티로 받아 실행한 onAdd() 함수에서 참조하는 this의 범위 때문에 발생한 것이다. onAdd()함수에 구현되어 있는 this.setState는 상위 컴포넌트에서 정의되어 있는데 하위 컴포넌트에서 실행되기 때문이다.

이 this 범위 오류는 bind() 함수를 이용하면 문제를 해결할 수 있다.

수정한 App.js

...
render(){
    return (
      <Counter count={this.state.count} onAdd={this.increaseCount.bind(this)}/>
    )
  }
}
...

이제 버튼이 제대로 동작한다. increaseCount() 함수가 호출되는 this 범위가 App 컴포넌트에 묶인 것이다. 이때 bind()함수는 constructor() 함수에 모아두면 매번 render() 함수를 호출할 때마다 새로 작성하지 않아도 된다. App 컴포넌트를 다음과 같이 수정해도 잘 동작한다.

...
constructor(props){
    super(props);
    this.state={
      count:1,
    };
    this.increaseCount=this.increaseCount.bind(this)
...
  render(){
    return (
      <Counter count={this.state.count} onAdd={this.increaseCount}/>
    )
  }
}
...

컴포넌트에서 DOM 객체 함수 사용하기

컴포넌트에서 window.addEventListener() 함수와 같은 DOM 객체 함수를 사용하려면 DOM 객체를 컴포넌트 변수에 할당해야 한다. 이때 특수 프로퍼티 ref를 사용한다. ref 프로퍼티는 document.getElementById()가 반환하는 객체를 반환한다. 단 ref 프로퍼티는 DOM 객체 함수가 필요한 엘리먼트에 콜백 함수로 전달된다.

다음은 화면에서 스크롤 위치를 측정해 현재 컴포넌트의 위치가 화면 안에 있는지 알려주는 컴포넌트이다. div 엘리먼트에 setRef() 함수를 콜백 함수 형태로 전달한 부분을 주목하자.

import React from 'react';

export default class ScrollSpy extends React.PureComponent{
    constructor(props){
        super(props);
        this.setRef = this.setRef.bind(this);
        this.checkPosition = this.checkPosition.bind(this);
        window.addEventListener('scroll',this.checkPosition); // 윈도우의 스크롤 이동 이벤트를 감지하여 checkPosition 함수가 실행되도록 함
    }
    setRef(ref){
        // ref 프로퍼티에서 반환된 객체를 컴포넌트 변수에 할당
        this.ref=ref;
    }
    checkPosition(){
        // 현재 DOM 객체의 위치가 스크롤 화면 안팎인지 측정
        if (this.ref.getBoundingClientRect().top < window.innerHeight){
            console.log('enter');
        } else{
            console.log('exit');
        }
    }
    componentDidMount(){
        this.checkPosition();
    }
    componentWillUnmount(){
        // 컴포넌트가 소멸할 때 화면 스크롤 감시 함수를 메모리에서 제거
        window.removeEventListener('scroll',this.checkPosition);
    }
    render(){
        return <div ref={this.setRef}/>
    }
}

위와 같은 코드는 페이스북에서 스크롤을 내릴 때 자동으로 다음 페이지 목록을 추가할 때 사용한다. 화면 스크롤이 변화할 때마다 객체 함수의 위치를 읽어 현재 위치가 화면 안팎인지 측정하고 있다. console.log('enter')에 해당하는 부분에 다음 페이지 목록을 추가하는 코드를 구현하면 스크롤이 내려갈 때마다 자동으로 다음 페이지 목록이 추가되도록 구현할 수 있다.

컴포넌트에서 DOM 이벤트 사용하기

컴포넌트에서 출력된 특정 DOM 객체에 이벤트 컴포넌트가 반응하기 위해서는 DOM 이벤트 프로퍼티를 사용해야 한다. 컴포넌트에 출력된 버튼이 클릭되었을 때 카운트 숫자가 증가하는 경우가 이러한 예이다. 이벤트 프로퍼티는 특수 프로퍼티로 콜백 함수 형태로 전달해 처리한다. 우리가 흔히 쓰는 HTML 엘리먼트의 이벤트들은 JSX 내에서 'on + 이벤트명' 형태이다.
onClick, onSubmit, onMouseMove, onMouseOver, onMouseOut, onKeyDown, onKeyPress 등등...

예제 :

import React from 'react';

class Counter3 extends React.Component {
    constructor(props){
        super(props);
        this.state={
            count:props.count
        }
        this.increaseCount = this.increaseCount.bind(this)
        this.resetCount = this.resetCount.bind(this)
    }
    increaseCount(){this.setState(({count})=>({count:count+1}))}
    resetCount(){this.setState({count:0})}
    render(){
        return (
            <div>
                현재 카운트 : {this.state.count}
                <button
                    onClick={this.increaseCount}
                    onMouseOut={this.resetCount}
                >
                    카운트 증가
                </button><br></br>
                버튼 밖으로 커서가 움직이면 0으로 초기화됩니다.
            </div>
        )
    }
}
export default Counter3;

단방향 흐름 방식 개념 정리

'데이터는 단방향으로 흐른다' 라고 이야기했던 내용을 배운 개념들과 함께 정리한 내용이다.
리액트는 프로퍼티, state와 같은 데이터를 상위 컴포넌트에서 하위 컴포넌트 방향으로 전달한다. 만약 데이터 변경이 필요한 경우 콜백 함수를 호출하여 원본 데이터가 위치한 상위 컴포넌트에서 데이터를 변경하고 다시 자식 컴포넌트로 전달하도록 만든다. 다음 그림은 그 과정을 보여준다.

위와 같은 데이터 흐름을 사용하는 기법을 '단방향 흐름 방식' 이라고 한다. 단방향 흐름 방식은 원본 데이터의 무결성을 지켜주므로 데이터 수정으로 인한 데이터 파편화를 줄여준다.

input 컴포넌트 만들기

import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';

class Input extends PureComponent {
  constructor(props){
    super(props);
    this.setRef = this.setRef.bind(this)
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(e){
    const {name,onChange} = this.props;
    if(onChange){
      onChange(name,e.target.value)
    }
  }
  componentDidMount(){
    if(this.props.autoFocus){
      this.ref.focus();
    }
  }
  componentDidUpdatae(){
    if(this.props.autoFocus){
      this.ref.focus()
    }
  }
  setRef(ref){
    this.ref=ref;
  }
  render(){
    const {errorMessage,label,name,value,type,onFocus} = this.props;
    return (
      <label>
        {label}
        <input
          id={`input_${name}`}
          ref={this.setRef}
          onChange={this.handleChange}
          onFocus={onFocus}
          value={value}
          type={type}
        />
        {errorMessage && <span className="error">{errorMessage}</span>}
      </label>
    )
  }
}
Input.propTypes = {
  type:PropTypes.oneOf(['text','number','price']),
  name: PropTypes.string.isRequired,
  value:PropTypes.oneOfType([PropTypes.number,PropTypes.string]),
  errorMessage:PropTypes.string,
  label:PropTypes.string,
  onChange:PropTypes.func,
  onFocus:PropTypes.func,
  autoFocus:PropTypes.bool,
}
Input.defaultProps={
  onChange:()=>{},
  onFocus:()=>{},
  autoFocus:false,
  type:'text',
}
export default Input;

후기

리액트 너무 재밋는것..
시간순삭잼 ㄷㄷ..

좋은 웹페이지 즐겨찾기