리액트 Summary - 1-2. 함수형 컴포넌트 기초 총정리-thumbnail

리액트 Summary - 1-2. 함수형 컴포넌트 기초 총정리

Functional Component summary
335

Functional Component

이번 설명에서 App.js는 거의 등장하지 않을 계획이므로, App.js는 굳이 함수화하지 않았다.

(src/App.js)

import { Component } from 'react';
import SampleFunctional from './SampleFunctional';

function getRandomColor() {
  return '#' + Math.floor(Math.random() * 16777215).toString(16);
}

class App extends Component {
  state = {
    color: '#000000'
  }

  handleClick = () => {
    this.setState({
      color: getRandomColor()
    })
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>Random Color Generator</button>
        <SampleFunctional color={this.state.color} idNumber={1}>SampleFunctional</SampleFunctional>
      </div>
    );
  }
}

export default App;

(src/SampleFunctional.js)

import { useState } from 'react';
import PropTypes from 'prop-types';

const SampleFunctional = ({ color, idNumber, children, ungivenProp }) => {
  const [number, setNumber] = useState(0);
  const [message, setMessage] = useState('');
  const [names, setNames] = useState({
    names: [
      {id: 1, text: 'Smith'},
      {id: 2, text: 'Kwang'},
      {id: 3, text: 'Leuitong'},
      {id: 4, text: 'Umnuwoa'},
      {id: 5, text: 'Alejandro'}
    ],
    nextId: 6,
    nameInput: ''
  });

  const handleClick = () => {
    setNumber((prevState, props) => {
      return prevState+1;
    });
    setNumber(prevState => prevState+1);
  }
  const handleChange = (e) => {
    if(e.target.name === 'message') setMessage(e.target.value);
    else if(e.target.name === 'nameInput') setNames({...names, nameInput: e.target.value})
  }
  const handleKeyPress = (e) => {
    if(e.key === 'Enter'){
      setMessage('');
    }
  }

  const handleKeyPressMap = (e) => {
    if(e.key === 'Enter'){
      setNames({...names,
        names: [
          ...names.names,
          { id: names.nextId, text: names.nameInput }
        ],
        nextId: names.nextId + 1,
        nameInput: '',
      })
    }
  }
  const handleDoubleClickFilter = (id) => {
    const nextNames = names.names.filter(name => name.names.id !== id);
    setNames({
      ...names,
      names: nextNames
    });
  }

  const name = 'Jinwoo';
  const style = {
    color: color
  };

  const namesList = names.names.map(name => <li key={name.id} onDoubleClick={() => handleDoubleClickFilter(name.id)}>{name.text}</li>)

  return (
    <>
      <h1>Hello, it's React.js example</h1>
      <div>
        <h2>Props part</h2>
        <p>You are {name}, right?</p>
        <p style={style}>And our parent gives us {color} props that defines text color of this sentence.</p>
        <p>Also children value: {children}</p>
        <p>If some prop is required, and when it's not given, there will be error in console. : {idNumber}</p>
        <p>If there is un given prop, you can give default value({ungivenProp}) to that prop.</p>
      </div>
      <div>
        <h2>State and Event part</h2>
        <p>Changing Number: {number}</p>
        <button
          className="number"
          onClick={handleClick}
        >increase +2</button>
        <input type="text" name="message"
          value={message}
          onChange={handleChange}
          onKeyPress={handleKeyPress}
        />
      </div>
      <div>
        <h2>map/filter function</h2>
        <ul>
          {namesList}
        </ul>
        <input type="text" name="nameInput"
          value={names.nameInput}
          onChange={handleChange}
          onKeyPress={handleKeyPressMap}
        />
      </div>
    </>
  );
}

SampleFunctional.defaultProps = {
  ungivenProp: 'Default Value'
}
SampleFunctional.propTypes = {
  name: PropTypes.string,
  idNumber: PropTypes.number.isRequired
}

export default SampleFunctional;

이전 class component 글을 본 사람이라면 이후의 설명에서 같은 말이 반복되는 것을 알 수 있을 것이다. 코드만 바뀌었지 설명이 동일한 부분들이 많아서 그대로 두었다. 원래는 아주 간단한 설명만 남기려고 하다가 혹시나 functional component만 보는 사람도 있을 수 있어 이전 class component에서 썼던 것들을 재사용했다.

1. props

지나치게 긴 위의 코드를 필요한 부분만 잘라서 보자.

(src/App.js)

import { Component } from 'react';
import SampleFunctional from './SampleFunctional';

function getRandomColor() {
  return '#' + Math.floor(Math.random() * 16777215).toString(16);
}

class App extends Component {
  state = {
    color: '#000000'
  }

  handleClick = () => {
    this.setState({
      color: getRandomColor()
    })
  }

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>Random Color Generator</button>
        <SampleFunctional color={this.state.color} idNumber={1}>SampleFunctional</SampleFunctional>
      </div>
    );
  }
}

export default App;

(src/SampleFunctional.js)

import PropTypes from 'prop-types';

const SampleFunctional = ({ color, idNumber, children, ungivenProp }) => {

  const style = {
    color: color
  };

  return (
    <>
      <h1>Hello, it's React.js example</h1>
      <div>
        <h2>Props part</h2>
        <p style={style}>And our parent gives us {color} props that defines text color of this sentence.</p>
        <p>Also children value: {children}</p>
        <p>If some prop is required, and when it's not given, there will be error in console. : {idNumber}</p>
        <p>If there is un given prop, you can give default value({ungivenProp}) to that prop.</p>
      </div>
    </>
  );
}

SampleFunctional.defaultProps = {
  ungivenProp: 'Default Value'
}
SampleFunctional.propTypes = {
  name: PropTypes.string,
  idNumber: PropTypes.number.isRequired
}

export default SampleFunctional;

상위 component에서 props 전달

Sample component의 상위 component인 App에서 color, idNumber 그리고 children props를 Sample component에 전달하고 있다. children prop은 <Sample> 태그 사이에 쓰여있는 text, 여기서는 SampleFunctional을 뜻한다.

import PropTypes from 'prop-types';

이를 전달받은 Sample component는 import PropTypes from 'prop-types'; 구문을 통해 PropTypes에 있는 메서드들을 활용, 다양한 설정을 해줄 수 있다.

(src/SampleFunctional.js)

import PropTypes from 'prop-types';

const SampleFunctional = ({ color, idNumber, children, ungivenProp }) => {
  (...)
}

SampleFunctional.defaultProps = {
  ungivenProp: 'Default Value'
}
SampleFunctional.propTypes = {
  name: PropTypes.string,
  idNumber: PropTypes.number.isRequired
}

export default SampleFunctional;

defaultProps

우선 Component.defaultProps를 정의함으로써, 혹시나 주어지지 않은 props에 대해 기본값을 설정해줄 수 있다.

propTypes

Component.propTypes를 통해 다양한 옵션들을 지정해줄 수 있다. 여기에는 전달받은 prop의 type 혹은 꼭 주어져야하는지 여부(isRequired) 등이 기록되게 된다.

Functional component의 인자로 받아온 { color, idNumber, children, ungivenProp }를 통해 props를 가져온다. 해당 props들은 {prop} 형태로 return문 내의 template에 활용하면 된다.

위의 형식 말고도 아래와 같이 위의 코드를 다시 써볼 수 있다. Destructuring assignment비구조화 할당을 통해 this.props로부터 부모가 전해준 props들을 전달받는다. 개인적으로는 약간 더 품이 듦으로 선호하지 않는다.

import PropTypes from 'prop-types';

const SampleFunctional = (props) => {
  const { color, idNumber, children, ungivenProp } = props;
  (...)
}

2. state

역시 필요한 부분만 잘라서 보자.

(src/SampleFunctional.js)

import { useState } from 'react';

const SampleFunctional = () => {
  const [number, setNumber] = useState(0);
  const [message, setMessage] = useState('');
  const [names, setNames] = useState({
    names: [
      {id: 1, text: 'Smith'},
      {id: 2, text: 'Kwang'},
      {id: 3, text: 'Leuitong'},
      {id: 4, text: 'Umnuwoa'},
      {id: 5, text: 'Alejandro'}
    ],
    nextId: 6,
    nameInput: ''
  });

  const handleClick = () => {
    setNumber((prevState, props) => {
      return prevState+1;
    });
    setNumber(prevState => prevState+1);
  }
  const handleChange = (e) => {
    if(e.target.name === 'message') setMessage(e.target.value);
    else if(e.target.name === 'nameInput') setNames({...names, nameInput: e.target.value})
  }
  const handleKeyPress = (e) => {
    if(e.key === 'Enter'){
      setMessage('');
    }
  }

  const handleKeyPressMap = (e) => {
    if(e.key === 'Enter'){
      setNames({...names,
        names: [
          ...names.names,
          { id: names.nextId, text: names.nameInput }
        ],
        nextId: names.nextId + 1,
        nameInput: '',
      })
    }
  }
  const handleDoubleClickFilter = (id) => {
    const nextNames = names.names.filter(name => name.names.id !== id);
    setNames({
      ...names,
      names: nextNames
    });
  }

  const name = 'Jinwoo';
  const style = {
    color: color
  };

  const namesList = names.names.map(name => <li key={name.id} onDoubleClick={() => handleDoubleClickFilter(name.id)}>{name.text}</li>)

  return (
    <>
      <h1>Hello, it's React.js example</h1>
      <div>
        <p>You are {name}, right?</p>
      <div>
        <h2>State and Event part</h2>
        <p>Changing Number: {number}</p>
        <button
          className="number"
          onClick={handleClick}
        >increase +2</button>
        <input type="text" name="message"
          value={message}
          onChange={handleChange}
          onKeyPress={handleKeyPress}
        />
      </div>
      <div>
        <h2>map/filter function</h2>
        <ul>
          {namesList}
        </ul>
        <input type="text" name="nameInput"
          value={names.nameInput}
          onChange={handleChange}
          onKeyPress={handleKeyPressMap}
        />
      </div>
    </>
  );
}

export default SampleFunctional;

변하지 않는 값

변하지 않는 값을 굳이 state에서 관리할 필요는 없다. Component 함수 내에 const name = 'Jinwoo';와 같이 써준 뒤 활용해줄 수 있다.

state

우선 해당 component에서 변화할 가능성이 있는 데이터들을 state 안에 담는다. 여기서는 Class Component와 다르게 import { useState } from 'react';를 통해 useState를 가져와서 state를 관리한다. 이는 Hooks의 일종인데, 차차 더 많은 Hooks를 배우게 될 것이다.

const [message, setMessage] = useState('');

Destructuring assignment가 여기서도 활용된다. 첫 번째 변수에는 state, 두 번째 변수에는 state를 수정하는 메서드가 담기게 된다. useState() 메서드 내부에는 initial value를 넣어준다.

useState

두 번째 변수를 활용해 state를 바꿀 때에는 아래와 같이 한다.

setMessage('연습하는 중입니다.');

Class component 때와 다른 점은, 두 번째 인자로 callback function을 줄 수 없다는 점이다. 같은 효과를 내고자 한다면 나중에 배울 useEffect가 필요하다. 혹시나 class component 때와 비교하고 싶은 분을 위해 두 코드를 모두 첨부한다. 첫번째 것이 functional component이고 두 번재가 class component이다.

const handleClick = () => {
  setNumber((prevState, props) => {
    return prevState+1;
  });
  setNumber(prevState => prevState+1);
}
handleClick = () => { // to avoid to use constructor initialization
  this.setState((prevState, props) => {
    return {number: prevState.number+1}
  }, () => console.log('first'));
  this.setState(prevState => ({
    number: prevState.number+1
  }), () => console.log('second'));
} 
prevState, props

아래 코드를 보자. 한 메서드 내에서 변경된 이후의 값에 다시 접근하고 싶다면 prevState, props(그 중에서도 prevState)를 활용해야 하여 굳이 예시를 좀 번거롭게 잡았다.

const handleClick = () => {
  setNumber((prevState, props) => {
    return prevState+1;
  });
  setNumber(prevState => prevState+1);
}

위의 코드를 보면 우선 prevState는 호출된 순간의 state 상태를 나타낸다. props는 현재 가지고 있는 props를 가리킨다. props가 불필요한 경우(사실 두 setNumber 모두 필요한 경우는 아니다) 두 번째 parameter인 props의 생략이 가능하다. 여기서는 각각 한 번씩 number를 증가시켜줌으로써 결과적으로 한 번의 클릭마다 state 내의 number 상태가 +2가 되는 것을 알 수 있다.

3. event

(src/SampleFunctional.js)

import { useState } from 'react';

const SampleFunctional = () => {
  (...)

  return (
    <>
      <h1>Hello, it's React.js example</h1>
      <div>
        <p>You are {name}, right?</p>
      <div>
        <h2>State and Event part</h2>
        <p>Changing Number: {number}</p>
        <button
          className="number"
          onClick={handleClick}
        >increase +2</button>
        <input type="text" name="message"
          value={message}
          onChange={handleChange}
          onKeyPress={handleKeyPress}
        />
      </div>
      <div>
        <h2>map/filter function</h2>
        <ul>
          {namesList}
        </ul>
        <input type="text" name="nameInput"
          value={names.nameInput}
          onChange={handleChange}
          onKeyPress={handleKeyPressMap}
        />
      </div>
    </>
  );
}

export default SampleFunctional;

event에서 활용되고 있는 parameter인 e는 syntheticEvent로, 웹 브라우저 내의 native event를 감싸고 있는 객체다. 다만 한 가지 다른 점은 이벤트가 끝난 뒤 초기화되다보니 비동기적으로 e를 참조할 수 없다. 그럴 때에는 syntheticEvent 객체 내의 persist() 메서드를 활용해주어야 한다.

CamelCase

onChange, onKeyPress 등 camel case 방식으로 선언해주고 있음을 볼 수 있다.

4. map & filter

(src/SampleFunctional.js)

import { useState } from 'react';

const SampleFunctional = ({ color, idNumber, children, ungivenProp }) => {
  const [names, setNames] = useState({
    names: [
      {id: 1, text: 'Smith'},
      {id: 2, text: 'Kwang'},
      {id: 3, text: 'Leuitong'},
      {id: 4, text: 'Umnuwoa'},
      {id: 5, text: 'Alejandro'}
    ],
    nextId: 6,
    nameInput: ''
  });

  const handleKeyPressMap = (e) => {
    if(e.key === 'Enter'){
      setNames({...names,
        names: [
          ...names.names,
          { id: names.nextId, text: names.nameInput }
        ],
        nextId: names.nextId + 1,
        nameInput: '',
      })
    }
  }
  const handleDoubleClickFilter = (id) => {
    const nextNames = names.names.filter(name => name.names.id !== id);
    setNames({
      ...names,
      names: nextNames
    });
  }

  const namesList = names.names.map(name => <li key={name.id} onDoubleClick={() => handleDoubleClickFilter(name.id)}>{name.text}</li>)

  return (
    <>
      <h1>Hello, it's React.js example</h1>
      <div>
        <h2>map/filter function</h2>
        <ul>
          {namesList}
        </ul>
        <input type="text" name="nameInput"
          value={names.nameInput}
          onChange={handleChange}
          onKeyPress={handleKeyPressMap}
        />
      </div>
    </>
  );
}

export default SampleFunctional;

map & filter

javascript의 내장 메서드 map을 활용해서 반복적인 요소를 쉽게 구현할 수 있다. jsx 문법에서만 가능한 코드의 모습이다. 또한 filter 함수를 이용해 조작하고 싶은 DOM 요소를 key 값을 활용해 콕 찝을 수 있다. 조금 길지만 쭉 훑어보면 어렵지 않은 내용이라 이 이상의 설명은 생략하도록 한다.

key

map을 통해 element를 반복적으로 만들어줄 때, key를 선언해야 한다. key는 언제나 유일한 값을 지녀야 한다. key는 virtual DOM을 비교하는 과정에서 이를 훨씬 수월하고 신속하게 해낼 수 있도록 도와주는 역할을 한다. 이를 설정하지 않는다면 console 창에 경고 메시지가 표시된다.


참고 서적 :
김민준, 2019, 길벗, 『리액트를 다루는 기술, 개정판』