프론트엔드/React

리액트 기초이론 2 - state & props

syleemomo 2021. 10. 22. 11:09
728x90

 

state 참고문서

 

컴포넌트 State – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

* state 개념 

컴포넌트 내부에서 변경이 가능한 데이터이다. 리액트에서는 state 를 변경함으로써 웹 페이지를 업데이트한다. 사용자 인터렉션을 처리하기 위하여 이벤트 핸들러 함수에서 state 를 변경한다. 예를 들면, 사용자가 버튼을 클릭했을때 state 를 변경하고 변경된 새로운 데이터를 이용하여 웹 화면을 리렌더링한다.  

 

* state 사용의 장점

기존 자바스크립트에서는 DOM 요소에 직접 접근하여 웹 페이지를 변경하였다. 이렇게 하면 전체 웹페이지에서 특정 DOM 을 검색하는데 시간이 걸리고, 변경해야 할 데이터가 많은 경우 DOM 이 여러번 업데이트 되면서 CPU 자원을 많이 사용하게 된다. 

* 브라우저가 화면을 렌더링하는데는 CPU 자원을 많이 사용한다.

<div id="contents">initial contents</div>
const contents = document.getElementById('contents')
contents.innerText = 'changed contents"

이에 비해 리액트에서는 특정 DOM 을 직접 검색하지 않고, state 를 이용하여 DOM 을 업데이트하므로 검색시간이 줄어든다. 또한, 변경할 데이터가 많은 경우 DOM 을 여러번 업데이트하지 않으므로 CPU 자원을 효율적으로 사용할 수 있다. 

import React, { Component } from 'react';

class Todo extends Component{
   state = {
    	name: "initial name"
    }
    render(){
    	const {name} = this.state
    	return (
            <>
            	<h1>{name}</h1>
            </>
        )
    }
 }
 export default Todo;

 

* state 변경 - setState 메서드

setState 메서드는 state 변경후 render 함수를 호출한다

state 를 변경할때는 컴포넌트의 setState 메서드를 사용하면 된다. setState 메서드는 캡슐화 되어 있으며, 단순하게 state 만 변경하는 것이 아니라 state 를 변경한 다음 render 함수를 호출하여 웹 화면을 리렌더링한다. 버튼을 클릭한 다음 개발자 도구에서 확인해보면 render 함수가 호출되면서 "NameTag" 라는 문자열이 출력된 것을 확인할 수 있다. 또한, 화면의 name 이 "changed name" 으로 변경된 것을 확인할 수 있다. 

import React, { Component } from 'react';

class NameTag extends Component{
   state = {
    	name: "initial name"
    }
    changeName = () => {
    	this.setState({name: "changed name"})
    }
    render(){
    	console.log('NameTag')
    	const {name} = this.state
    	return (
            <>
            	<h1>{name}</h1>
                <button type="button" onClick={this.changeName}>change name</button>
            </>
        )
    }
 }
 export default NameTag;
import logo from './logo.svg';
import './App.css';
import NameTag from './NameTag'

function App() {
  return (
    <div className="App">
     <NameTag></NameTag>
    </div>
  );
}

export default App;

버튼을 클릭한 결과

 

state 를 직접 변경하면 render 함수는 호출되지 않는다

아래와 같이 state 를 직접 변경하는 경우 버튼을 클릭했을때 'NameTag" 문자열이 출력되지 않는다. 즉, state 를 직접 변경하는 경우 render 함수는 호출되지 않는다. 또한, 화면의 name 이 변경되지 않고 "initial name" 이 유지된다. 또한, 콘솔에 "Do not mutate state directly. Use setState()  react/no-direct-mutation-state" 라는 경고 메세지가 표시된다. 

import React, { Component } from 'react';

class NameTag extends Component{
   state = {
    	name: "initial name"
    }
    changeName = () => {
    	this.state.name = "changed name"
    }
    render(){
    	console.log('NameTag')
    	const {name} = this.state
    	return (
            <>
            	<h1>{name}</h1>
                <button type="button" onClick={this.changeName}>change name</button>
            </>
        )
    }
 }
 export default NameTag;

state 를 직접 변경한 경우 경고 메세지 출력

 

setState 메서드를 render 함수 안에서 호출하면 무한루프에 빠진다

바로 위의 예제 코드와 비교해서 변경된 부분이 없는것처럼 보인다. 하지만, button 요소의 onClick 속성을 보면 this.changeName 에서 this.changeName() 으로 변경된 것을 확인할 수 있다. 이렇게 하면 사용자가 클릭할때마다 호출되는 것이 아니라 프로그램이 실행될때 곧바로 이벤트 핸들러 함수가 호출된다.

import React, { Component } from 'react';

class NameTag extends Component{
   state = {
    	name: "initial name"
    }
    changeName = () => {
        console.log('clicked !')
    	this.setState({name: "changed name"})
    }
    render(){
    	console.log('NameTag')
    	const {name} = this.state
    	return (
            <>
            	<h1>{name}</h1>
                <button type="button" onClick={this.changeName()}>change name 2</button>
            </>
        )
    }
 }
 export default NameTag;

초반에 한번만 실행되는 이벤트 핸들러 함수

 

changeName 이라는 이벤트 핸들러가 호출되면 setState 메서드가 호출된다. 그럼 state 를 변경한 다음 render 함수를 호출하게 된다. render 함수 안에서 다시 changeName 이라는 이벤트 핸들러가 호출되고 setState 가 다시 호출되고, 당연히 render 함수가 또 호출된다. 결국 무한루프에 빠지게 된다. 

setState 를 render 함수 안에서 호출한 모습

무한루프가 영원히 동작하는것이 아니라 최대로 반복될 수 있는 횟수가 정해져 있기 때문에 그 횟수를 초과하게 되면 에러 메세지를 출력한다. 

 

자바스크립트에서 비동기 동작방식

 

setState 메서드는 비동기로 동작한다

setState 메서드로 변경된 state 값은 render 함수 안에서 확인 가능하다

import React, { Component } from 'react'

class Counter extends Component{
    state = {
        count: 0
    }
    increaseA =  async () => {
        const result = await fetch('https://jsonplaceholder.typicode.com/todos/1')
        .then(res => res.json())
        console.log('첫번째 함수')
        console.log(result)
    }
    increaseB = () => {
        console.log('두번째 함수')
    }
    handleClick = () => {
        this.increaseA()
        this.increaseB()
    }
    
    render(){
        const { count } = this.state 
        return (
            <>
                <h1>{count}</h1>
                <button onClick={this.handleClick}>카운팅 버튼</button>
            </>
        ) 
    }
}
export default Counter

setState 메서드는 비동기로 동작한다. setState 함수를 실행한다고 해서 곧바로 state 가 변경되지는 않는다. setState 함수는 callback queue 에 등록되었다가 모든 함수들이 실행된 다음 callstack 이 비어있게 되면 event loop 가 이를 감지하고 callback queue 에서 setState 함수를 callstack 으로 옮겨서 실행하게 된다.

import React, { Component } from 'react';

class Counter extends Component{
   state = {
    	count: 0
    }
    increase = () => {
    	const {count} = this.state
    	console.log(`before updating state: ${count}`)
    	this.setState({ count: count + 1})
        console.log(`after updating state: ${count}`)
    }
    render(){
    	const {count} = this.state
        console.log(`state in render function: ${count}`)
    	return (
            <>
            	<h1>{count}</h1>
                <button type="button" onClick={this.increase}>increase count</button>
            </>
        )
    }
 }
 export default Counter;

버튼을 클릭해서 개발자 도구의 콘솔창을 확인해보자!

import logo from './logo.svg';
import './App.css';
import Counter from './Counter'

function App() {
  return (
    <div className="App">
     <Counter></Counter>
    </div>
  );
}

export default App;

컴포넌트에서 setState 메서드를 호출하기 직전과 직후에 count 값을 출력해보면 값이 똑같다. 곧바로 state 가 변경되지 않음을 확인할 수 있다. 또한, setState 가 비동기로 실행되고 나면 render 함수가 호출되면서 변경된 state 값을 이용하여 리렌더링한다. 즉, render 함수에서 변경된 state 값을 확인할 수 있다. render 함수에서 count 값을 출력해보면 값이 증가했음을 확인할 수 있다. 

버튼을 클릭하고 콘솔창을 확인해보자

 

setState 메서드는 이벤트 핸들러 함수 안에서 state 를 변경하지 않고 render 함수에서 변경한다

setState 메서드를 동시에 여러번 호출하면 render 함수는 마지막에 한번만 호출된다

만약 setState 메서드를 동시에 여러번 호출하면 어떻게 될까? increaseMultiple 이라는 이벤트 핸들러 함수를 만들고 사용자가 버튼을 클릭했을때 setState 메서드를 여러번 호출해보자!

import React, { Component } from 'react';

class Counter extends Component{
   state = {
    	count: 0
    }
    increase = () => {
    	const {count} = this.state
    	console.log(`before updating state: ${count}`)
        this.setState({ count: count + 1})
        console.log(`after updating state: ${count}`)
    }
    increaseMultiple = () => {
        // ------------ 업데이트 되지 않는 구간 -----------------//
        this.increase() // setState 인자로 객체를 사용하면 업데이트 안된다(0 -> 1)    
        this.increase() // setState 인자로 객체를 사용하면 업데이트 안된다(0 -> 1)  
        this.increase() // setState 인자로 객체를 사용하면 업데이트 안된다(0 -> 1)
        
        console.log(`right after event: ${this.state.count}`)
        // ----------------------------------------------------//
    }
    render(){
        // 여기서 state 가 업데이트된다
        // 모든 이벤트가 종료되는 이 시점에서 state 를 변경하고 render 함수를 한번만 호출한다
        console.log('counter')
    	const {count} = this.state
        console.log(`state in render function: ${count}`)
    	return (
            <>
            	<h1>{count}</h1>
                <button type="button" onClick={this.increaseMultiple}>increase count</button>
            </>
        )
    }
 }
 export default Counter;
import logo from './logo.svg';
import './App.css';
import Counter from './Counter'

function App() {
  return (
    <div className="App">
     <Counter></Counter>
    </div>
  );
}

export default App;

setState 인자로 state 객체를 사용한 경우

충격적이다! 우리가 예상한 값은 3인데 1이 카운팅되었다. 일단 하나씩 살펴보자!

먼저 setState 메서드를 호출할때마다 render 함수도 호출된다고 설명했다. 그럼 setState 메서드를 3번 호출했으니 render 함수도 3번 호출되어야 한다. 그치만 콘솔창을 확인해보면 render 함수 안의 "counter" 문자열과 "state in render function: 1" 출력이 한번만 되었음을 확인할 수 있다. 즉, render 함수는 마지막에 1번만 호출되었다.

리액트에서 이렇게 동작하게 만든 이유는 브라우저가 화면을 동시에 여러번 업데이트할 경우 CPU 자원을 많이 소모하기 때문이다. 그래서 리액트는 여러개의 변경사항을 모아두었다가 마지막에 한번만 화면을 업데이트하도록 설계되었다.  이렇게 하면 CPU 자원을 덜 소모하고 효율적으로 웹 화면을 렌더링할 수 있기 때문이다. 이것은 마치 화가가 수정사항이 있을때마다 캔버스에서 그림을 수정하지 않고, 모든 수정사항을 모아두었다가 한번에 그림을 수정하는 것에 비유된다.

increase = () => {
    	const {count} = this.state
    	console.log(`before updating state: ${count}`)
        this.setState({ count: 0 + 1})
        console.log(`after updating state: ${count}`)
    }

그럼 이제 왜 우리가 예상한 값인 3이 아니라 1이 출력되었는지 알아보자!  이전 예제에서 setState 메서드는 비동기로 동작한다고 하였다. 바꿔 말하면 setState 메서드로 state 를 변경하는 시점은 이벤트 핸들러 함수 내부가 아니라 render 함수가 호출되었을때다. 즉, render 함수 안에서 변경된 state 값을 읽을수 있다. 이벤트 핸들러 함수인 increase 나 increaseMultiple 에서는 state 변경이 일어나지 않으므로 this.state.count 값은 0 을 유지한다. 그러다가 increaseMultiple 메서드의 실행이 끝나고 나서 increase 메서드 안의 setState 가 3번 실행된다. 맨 처음 화면이 초기 렌더링될때 초기값으로 설정된 count값(0)이 위 코드와 같이 increase 메서드 안의 setState 안에 설정되기 때문에 setState 를 3번 실행되더라도 결과적으로 count 값은 1로 설정된다. render 함수가 호출되면 this.state.count 값은 1이므로 해당값으로 리렌더링한다. 

 

그럼 우리가 예상한 값을 출력하려면 어떻게 하면 될까?

import React, { Component } from 'react';

class Counter extends Component{
   state = {
    	count: 0
    }
    increase = () => {
    	const {count} = this.state
    	console.log(`before updating state: ${count}`)
        this.setState( (prevState) => {
            return {count: prevState.count + 1}
        })
        console.log(`after updating state: ${count}`)
    }
    increaseMultiple = () => {
        // ------------ 업데이트 되지 않는 구간 -----------------//
        this.increase() // updater 사용시 this.state.count 는 업데이트되지 않지만 prevState 는 계속 업데이트된다 (0 -> 1)
        this.increase() // updater 사용시 this.state.count 는 업데이트되지 않지만 prevState 는 계속 업데이트된다 (1 -> 2)
        this.increase() // updater 사용시 this.state.count 는 업데이트되지 않지만 prevState 는 계속 업데이트된다 (2 -> 3)
        
        console.log(`right after event: ${this.state.count}`)
        // ----------------------------------------------------//
    }
    render(){
        // 여기서 state 가 업데이트된다
        // 모든 이벤트가 종료되는 이 시점에서 state 를 변경하고 render 함수를 한번만 호출한다
        console.log('counter')
    	const {count} = this.state
        console.log(`state in render function: ${count}`)
    	return (
            <>
            	<h1>{count}</h1>
                <button type="button" onClick={this.increaseMultiple}>increase count</button>
            </>
        )
    }
 }
 export default Counter;

setState 메서드의 인자로 state 객체 대신 콜백함수를 전달할 수 있다. 리액트에서는 이 콜백함수를 updater 함수라고 한다. updater 함수를 사용하면 언제나 최신의 state 값을 조회할 수 있다. update 함수의 파라미터로 업데이트된 최신 state 값이 전달된다.

increase 와 increaseMultiple 이벤트 핸들러 함수에서 this.state.count 값은 언제나 0 이지만 prevState 값은 계속 업데이트된다. 마지막에 render 함수가 호출되면 this.state.count 값은 최신의 prevState 값 3을 전달받아 3이 된다. 

setState 인자로 updater 함수를 사용한 경우

 

 

부모 컴포넌트에서 setState 메서드를 호출하면 부모와 자식 컴포넌트 모두 리렌더링된다

import React, { Component } from 'react';

class Counter extends Component{
   state = {
    	count: 0
    }
    increase = () => {
    	const {count} = this.state
    	console.log(`before updating state: ${count}`)
    	this.setState({ count: count + 1})
        console.log(`after updating state: ${count}`)
    }
    render(){
        console.log('child')
    	const {count} = this.state
        console.log(`state in render function: ${count}`)
    	return (
            <>
            	<h1>{count}</h1>
                <button type="button" onClick={this.increase}>increase count</button>
            </>
        )
    }
 }
 export default Counter;

만약 부모 컴포넌트인 App 에서 state 를 변경하면 어떻게 될까? 

import logo from './logo.svg';
import './App.css';
import React, { Component } from 'react';
import Counter from './Counter'

class App extends Component {
  state = {
    name: 'parent'
  }
  changeName = () => {
    this.setState({name: "parent changed"})
  }
  render(){
    console.log('parent')
    const {name} = this.state
    return (
      <div className="App">
        <h1>{name}</h1>
        <button type="button" onClick={this.changeName}>change name</button>
        <Counter></Counter>
      </div>
    )
  }
}

export default App;

부모 컴포넌트에서 버튼을 클릭하면 App 컴포넌트의 setState 메서드가 비동기로 실행되면서 App 컴포넌트의 render 함수를 호출한다. 그럼 "parent" 라는 문자열이 콘솔에 출력된다. 동시에 Counter 컴포넌트의 render 함수도 호출되면서 "child" 라는 문자열도 콘솔에 출력된다. 즉, 부모 컴포넌트에서 state 를 변경하면 부모만 리렌더링되는 것이 아니라 부모의 render 함수 안에 존재하는 모든 child 컴포넌트들도 함께 리렌더링된다. 

부모의 state 변경시 출력확인

 

 

* state 변경시 새로운 배열이나 객체를 생성하는 이유

state 변경시 배열이나 객체를 업데이트할때 새로운 배열이나 객체의 참조값을 전달하는 이유는 리액트 내부적으로 state 가 변경되었음을 인지하려면 이전과 완전히 다른값이어야 하기 때문이다. 즉, 배열이나 객체의 참조값이 완전히 달라야 리액트는 state 가 바뀌었다고 인식한다. 

 

* props 의 개념

컴포넌트의 속성으로 전달되는 값이다. 컴포넌트 중에는 내부에서 state 를 변경할 필요없이 전달되는 데이터(props)를 이용하여 HTML 템플릿에 렌더링만 하는 것도 있기 때문이다. props 값은 컴포넌트 내부에서 수정이 불가능하다. 

import React, { Component } from 'react';

class Counter extends Component{
    render(){
        this.props = {user: "성용"}
        console.log(this.props)
    	return (
            <>
            	<h1>Props 변경하기</h1>
            </>
        )
    }
 }
 export default Counter;

Counter 컴포넌트를 위와 같이 수정하자!

import logo from './logo.svg';
import './App.css';
import React, { Component } from 'react';
import Counter from './Counter'

class App extends Component {
  render(){
    return (
      <div className="App">
        <Counter user="sunrise"></Counter>
      </div>
    )
  }
}

export default App;

App 컴포넌트를 위와 같이 수정하자!

Props 를 변경하려는 경우 발생하는 경고 메시지

props 를 변경할수는 있지만 리액트에서는 위와 같은 경고 메세지를 보여준다. 즉, 변경하면 프로그램 버그가 발생할 수 있다고 경고한다.

 

* props 사용예시

import React, { Component } from 'react';

function YoutubeVideo({videoId, videoName, videoLength, videoDescription}){
    return (
        <div id={videoId}>
            <h1>{videoName} - {(parseInt(videoLength)/1000).toFixed(1)} MB</h1>
            <p>{videoDescription}</p>
        </div>
    )
}
 
export default YoutubeVideo;

src 폴더에 YoutubeVideo 컴포넌트를 추가하자!

import logo from './logo.svg';
import './App.css';
import YoutubeVideo from './YoutubeVideo'

function App() {
  return (
    <div className="App">
     <YoutubeVideo videoId="1234567890" 
                   videoName="Christmas dance" 
                   videoLength="23765" 
                   videoDescription="it is dance video on christmas party"
    ></YoutubeVideo>
    </div>
  );
}

export default App;

유튜브 영상에 대한 데이터를 가져와서 컴포넌트에서 렌더링한다고 해보자! 

YoutubeVideo 컴포넌트 속성으로 videoId, videoName, videoLength, videoDescription 이 설정된다. 이 값들은 컴포넌트 내부로 전달되는 props 이다. 컴포넌트 내부에서는 해당 props 데이터를 렌더링만 해주면 된다. 결과는 아래와 같다.

YoutubeVideo 컴포넌트 출력 결과

 

만약 서버에서 데이터를 가져와서 렌더링해야 한다면 어떻게 할까? 서버에서 직접 데이터를 가져오는 대신에 프로젝트 루프 폴더 아래 dummyData.js 라는 파일을 생성하고 아래와 같은 가상의 서버 데이터를 추가해보자!

function generateRandomId(n){
    const nums = new Array(n).fill(0)
    return nums.map(n => Math.floor(Math.random()*10)).join("")
}
function generateRandomString(n){
    const alphabet = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
    const str = new Array(n).fill('a')
    return str.map(s => alphabet[Math.floor(Math.random()*alphabet.length)]).join("")
}

const dummyData = [
    {
        videoId: generateRandomId(16), 
        videoName: generateRandomString(10), 
        videoLength: generateRandomId(6), 
        videoDescription: generateRandomString(100)
    },
    {
        videoId: generateRandomId(16), 
        videoName: generateRandomString(10), 
        videoLength: generateRandomId(6), 
        videoDescription: generateRandomString(100)
    },
    {
        videoId: generateRandomId(16), 
        videoName: generateRandomString(10), 
        videoLength: generateRandomId(6), 
        videoDescription: generateRandomString(100)
    },
    {
        videoId: generateRandomId(16), 
        videoName: generateRandomString(10), 
        videoLength: generateRandomId(6), 
        videoDescription: generateRandomString(100)
    },
    {
        videoId: generateRandomId(16), 
        videoName: generateRandomString(10), 
        videoLength: generateRandomId(6), 
        videoDescription: generateRandomString(100)
    }
]
export default dummyData;

랜덤함수를 이용하여 videoId, videoName, videoLength, videoDescription 값들을 랜덤 숫자와 랜덤 문자열로 생성하였다. 

function generateRandomId(n){
    const nums = new Array(n).fill(0)
    return nums.map(n => Math.floor(Math.random()*10)).join("")
}

파라미터로 주어진 n 개의 랜덤숫자를 생성하는 코드이다. 첫번째 줄은 n 개의 배열 요소를 생성하고 모두 0으로 초기화한다는 의미다. 두번째 줄은 배열을 순회하면서 각 요소마다 0~9 까지의 숫자중 랜덤숫자를 선택한다. join 함수는 랜덤숫자들의 배열을 하나의 문자열로 변환한다. 

function generateRandomString(n){
    const alphabet = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
    const str = new Array(n).fill('a')
    return str.map(s => alphabet[Math.floor(Math.random()*alphabet.length)]).join("")
}

파라미터로 주어진 n 개의 랜덤문자를 생성하는 코드이다. 첫번째 줄은 알파벳 문자를 배열로 초기화한다. 두번째 줄은 n 개의 배열 요소를 생성하고 모두 문자 "a"로 초기화한다. 세번째 줄은 배열을 순회하면서 각 요소마다 알파벳 a~z 중 랜덤문자를 선택한다. join 함수는 랜덤문자들의 배열을 하나의 문자열로 변환한다. 

import logo from './logo.svg';
import './App.css';
import YoutubeVideo from './YoutubeVideo'
import dummyData from './dummyData';

function App() {
  return (
    <div className="App">
    {dummyData.map(d => {
      return(
        <YoutubeVideo 
              key={d.videoId}
              videoId={d.videoId}
              videoName={d.videoName}
              videoLength={d.videoLength}
              videoDescription={d.videoDescription}
      ></YoutubeVideo>
      )
      }
    )}
     
    </div>
  );
}

export default App;

App 컴포넌트를 위와 같이 수정한다.  랜덤하게 생성한 dummyData 를 가져와서 각각의 데이터를 순회하면서 YoutubeVideo 컴포넌트에 서로 다른 데이터를 삽입한다. 그러면 삽입한 데이터는 YoutubeVideo 컴포넌트 내에서 props 로 가져와서 렌더링하면 된다. 결국 위 코드는 배열 데이터를 HTML 문서로 변환한 것이다. 중괄호({})를 사용하면 JSX 태그 안에서 자바스크립트 표현식(문법)을 사용할 수 있다.

마지막으로 같은 컴포넌트를 여러번 렌더링할때는 반드시 key 속성을 설정해줘야 한다. 그렇지 않으면 에러나 경고 메세지를 콘솔에 출력한다. 여기서는 YoutubeVideo 컴포넌트를 여러번 렌더링하므로 key 속성의 값으로 videoId 를 설정하였다.  

가짜 데이터로 유튜브 영상 데이터를 보여준 결과

 

 

* props 유효성 검증

설치 가이드

 

prop-types

Runtime type checking for React props and similar objects.

www.npmjs.com

props 타입체크 참고문서

 

PropTypes와 함께 하는 타입 검사 – React

A JavaScript library for building user interfaces

ko.reactjs.org

YoutubeVideo 컴포넌트 내부로 전달되는 props 에 타입체크를 해서 데이터 유효성을 검증해보자 !

import PropTypes from 'prop-types';

function YoutubeVideo({videoId, videoName, videoLength, videoDescription}){
    return (
        <div id={videoId}>
            <h1>{videoName} - {(parseInt(videoLength)/1000).toFixed(1)} MB</h1>
            <p>{videoDescription}</p>
        </div>
    )
}
 
export default YoutubeVideo;

YoutubeVideo.propTypes = {
    videoId: PropTypes.string,
    videoName: PropTypes.string,
    videoLength: PropTypes.number,
    videoDescription: PropTypes.string
}

prop-types 패키지를 사용하면 컴포넌트 내부로 전달받는 props 데이터의 유효성을 검증할 수 있다. 만약 videoLength 데이터를 숫자 타입으로 전달받고 싶다면 위와 같이 PropTypes 객체의 프로퍼티를 number 로 설정하면 된다. 위에서 설명했듯이 랜덤함수로 생성한 가짜 데이터는 모두 문자열 타입이다. 그러므로 videoLength props 는 숫자 타입을 기대했지만 문자열 타입의 videoLength 데이터가 전달되므로 아래와 같은 경고 메세지가 출력된다. 

props 타입체크 경고 메세지

 

* props 기본값 설정

만약 props 의 기본값을 설정하려면 어떻게 하면 될까?

import PropTypes from 'prop-types';

function YoutubeVideo({videoId, videoName, videoLength, videoDescription, videoAuthor}){
    return (
        <div id={videoId}>
            <h1>{videoName} - {(parseInt(videoLength)/1000).toFixed(1)} MB</h1>
            <h3>author - {videoAuthor}</h3>
            <p>{videoDescription}</p>
        </div>
    )
}
 
export default YoutubeVideo;

YoutubeVideo.propTypes = {
    videoId: PropTypes.string,
    videoName: PropTypes.string,
    videoLength: PropTypes.number,
    videoDescription: PropTypes.string
}

YoutubeVideo.defaultProps = {
    videoAuthor: "syleemomo"
}

위와 같이 컴포넌트의 프로퍼티로 defaultProps 를 설정해주면 된다. videoAuthor 로 전달되는 값이 있으면 해당 값을 사용하고 videoAuthor 값이 null 이나 undefined 등의 값이면 defaultProps 로 설정한 값을 사용한다. 결과는 아래와 같다. 

props 디폴트값 설정

 

 

* props 로 컨텐츠 삽입하기

유튜브 영상삽입 참고블로그

 

[HTML기초문법] 8강 iframe태그와 youtube영상 넣기 및 옵션 설정

** 영상으로 보고 싶은 분은 아래 주소를 클릭하세요. https://www.youtube.com/watch?v=VVVmPjnqT8U 1. youtube영상을 HTML에 넣기 1) 유튜브 사이트에 접속 주소 : https://www.youtube.com/ YouTube www.youtu..

ossam5.tistory.com

<h1>Hello world !</h1>

시작태그와 종료태그 사이에 있는 문자열이나 HTML 요소를 컨텐츠라고 한다. 

<YoutubeVideo 
              key={d.videoId}
              videoId={d.videoId}
              videoName={d.videoName}
              videoLength={d.videoLength}
              videoDescription={d.videoDescription}>
 // 컨텐츠 삽입       
</YoutubeVideo>

그럼 컴포넌트의 컨텐츠를 컴포넌트 내부로 전달하려면 어떻게 하면 될까?

import PropTypes from 'prop-types';

function YoutubeVideo({videoId, videoName, videoLength, videoDescription, videoAuthor, children}){
    return (
        <div id={videoId}>
            <h1>{videoName} - {(parseInt(videoLength)/1000).toFixed(1)} MB</h1>
            <h3>author - {videoAuthor}</h3>
            <p>{videoDescription}</p>
            {children}
        </div>
    )
}
 
export default YoutubeVideo;

YoutubeVideo.propTypes = {
    videoId: PropTypes.string,
    videoName: PropTypes.string,
    videoLength: PropTypes.string,
    videoDescription: PropTypes.string
}

YoutubeVideo.defaultProps = {
    videoAuthor: "syleemomo"
}

YoutubeVideo 컴포넌트를 위와 같이 수정하자! props 에 children 이 추가되었다. 그리고 HTML 템플릿에 children 을 삽입하였다. 이렇게 하면 YoutubeVideo 컴포넌트의 컨텐츠를 props 로 전달받을 수 있다. 

import './App.css';
import YoutubeVideo from './YoutubeVideo'
import dummyData from './dummyData';

function App() {
  return (
    <div className="App">
      {dummyData.map(d => {
        return (
          <YoutubeVideo
            key={d.videoId}
            videoId={d.videoId}
            videoName={d.videoName}
            videoLength={d.videoLength}
            videoDescription={d.videoDescription}
          >
            <iframe width="560" height="315" src="https://www.youtube.com/embed/sqgxcCjD04s" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen></iframe>
          </YoutubeVideo>
        )
      }
      )}

    </div>
  );
}

export default App;

유튜브 영상삽입 참고블로그를 보고 YoutubeVideo 컴포넌트의 컨텐츠로 유튜브 영상을 삽입하였다. 해당 영상은 YoutubeVideo 컴포넌트 내부에 children props 로 전달된다. 

YoutubeVideo 컴포넌트에 컨텐츠로 유튜브 영상을 삽입한 모습

 

 

* state 와 props 를 사용하는 시점

state 는 컴포넌트 내부에서 동적으로 변경되어야 할 데이터가 있는 경우 사용하면 되고, props 는 컴포넌트 내부에서 정적으로 데이터를 화면에 렌더링만 할 경우에 사용하면 된다. 

 

* state 와 props 의 관계

부모 컴포넌트의 state 값을 자식 컴포넌트의 props 로 전달할 수 있다. 

import logo from './logo.svg';
import './App.css';
import React, { Component } from 'react';
import Child from './Child'

class App extends Component {
  state = {
    name: 'parent',
    childName: "child"
  }
  changeName = () => {
    this.setState({name: "parent changed", childName: "child changed too"})
  }
  render(){
    console.log('parent')
    const {name, childName} = this.state
    return (
      <div className="App">
        <h1>{name}</h1>
        <button type="button" onClick={this.changeName}>change name</button>
        <Child name={childName}></Child>
      </div>
    )
  }
}

export default App;

App 컴포넌트를 위와 같이 작성하자! 

import React from 'react';

function Child({name}){
    return (
        <>
            <h1>{name}</h1>
        </>
    )
}
 
export default Child;

Child 컴포넌트도 만들자! 이제 버튼을 클릭해보자! 어떤 변화가 있는가?

버튼을 클릭하기 전의 모습
버튼을 클릭한 후의 모습

버튼을 클릭하면 changeName 이벤트핸들러 함수가 호출되면서 setState 메서드로 state 를 변경한다. 그럼 App 컴포넌트의 render 함수가 호출되면서 변경된 name, childName 를 삽입하고 리렌더링한다. childName 이라는 state 값은 Child 컴포넌트의 props 로 전달된다. 즉, Child 컴포넌트 내부에서 name 속성을 조회하면 된다.

Child 컴포넌트는 부모 컴포넌트에서 변경한 state 값을 Child 컴포넌트에 props 로 전달함으로써 Child 컴포넌트의 UI를 변경하였다.  

 

* 연습과제 1

App.js 파일을 아래와 같이 작성하고 실행하면 카운터 프로그램이 동작하지 않는다. 아래 코드를 수정해서 카운터가 제대로 동작하도록 해보자!

import logo from './logo.svg';
import './App.css';
import React, { Component } from 'react';

class App extends Component {
  state = {
    cnt: 0
  }
  countNumber = () => {
    this.setState({cnt: this.state.cnt + 1})
  }
  render(){
    const {cnt} = this.state
    return (
      <div className="App">
        <h1>카운트: {cnt}</h1>
        <button type="button" onClick={this.countNumber()}>change name</button>
      </div>
    )
  }
}

export default App;

카운터 프로그램 에러 메세지 출력

 

* 연습과제 2

아래 코드에서 버튼을 클릭하면 제목이 변경되지 않는다. 버튼 클릭시 제목이 변경되도록 해보자!

import logo from './logo.svg';
import './App.css';
import React, { Component } from 'react';

class App extends Component {
  state = {
    title: "변경전 제목"
  }
  changeTitle = () => {
    this.state.title = "제목 업데이트"
  }
  render(){
    const {title} = this.state
    return (
      <div className="App">
        <h1>제목: {title}</h1>
        <button type="button" onClick={this.changeTitle}>change title</button>
      </div>
    )
  }
}

export default App;

 

* 연습과제 3

아래 코드는 버튼을 클릭할때마다 쇼핑카트에 고객이 원하는 상품을 추가하고, 화면에 전체 상품목록을 보여주는 Cart 컴포넌트의 일부분이다. 코드를 완성해서 해당 기능이 정상적으로 동작하도록 해보자! (prompt 함수를 이용하여 사용자로부터 원하는 상품을 입력받는다)

import React, { Component } from 'react'

class Cart extends Component{
    state = {
        cart: []
    }

    //구현하기
    addProduct = () => {
        const product = prompt("원하시는 상품을 추가해주세요 !")
    }

    // 구현하기
    render(){
        const { cart } = this.state 
    }
}
export default Cart
import logo from './logo.svg';
import './App.css';
import React, { Component } from 'react';
import Cart from './Cart'

class App extends Component {
  render(){
    return (
      <div className="App">
       <Cart/>
      </div>
    )
  }
}

export default App;

카트에 담긴 상품목록 보여주기

 

* 연습과제 4

아래 코드는 버튼을 클릭할때마다 포토 갤러리에 사진을 추가하고, 화면에 전체 사진 리스트를 보여주는 PhotoGallery 컴포넌트의 일부분이다. 코드를 완성해서 해당 기능이 정상적으로 동작하도록 해보자! (prompt 함수를 이용하여 아래와 같이 사용자로부터 이미지 주소를 입력받는다)

import React, { Component } from 'react'

class PhotoGallery extends Component{
    state = {
        photos: []
    }

    //구현하기
    addPhoto = () => {
        const photo = prompt("추가하려는 사진의 경로를 입력해주세요 !")
    }

    // 구현하기
    render(){
        
    }
}
export default PhotoGallery
import logo from './logo.svg';
import './App.css';
import React, { Component } from 'react';
import PhotoGallery from './PhotoGallery'

class App extends Component {
  render(){
    return (
      <div className="App">
       <PhotoGallery/>
      </div>
    )
  }
}

export default App;

 

이미지 주소 복사하기
복사한 이미지 주소 입력하기
전체 사진 리스트 화면에 보여주기

 

* 연습과제 5

아래 코드는 props 로 주어진 댓글을 필터링하는 CommentFilter 컴포넌트이다. 하지만 댓글 필터링 기능이 제대로 동작하지 않는다. 코드를 수정해서 댓글 필터링 기능이 잘 동작하도록 해보자!

import React, { Component } from 'react'

class CommentFilter extends Component{
    state = {
        comment: this.props.comment.split(' '),
        insults: ['바보', '똥개', '그지', '임마', '새끼', '죽을']
    }
    filterComment = () => {
        const { insults } = this.state // 댓글을 음절 단위로 끊기

        for(let insult of insults){ // 욕설 필터링
            this.setState({comment: this.state.comment.filter(word => !word.includes(insult)) })
        }
    }
    componentDidMount(){
        this.filterComment()
    }
    render(){
        const { comment } = this.state
        console.log(comment)
        return (
            <>
                <h2>{comment.join(' ')}</h2>
            </>
        )
    }
}
export default CommentFilter
import logo from './logo.svg';
import './App.css';
import React, { Component } from 'react';
import CommentFilter from './CommentFilter'

class App extends Component {
  render(){
    return (
      <div className="App">
        <h1>필터링된 댓글</h1>
        <h2>==============</h2>
       <CommentFilter comment="너는 진짜 못말리는 바보 똥개다"/>
       <CommentFilter comment="임마! 너 그렇게 살지마! 그지 새끼야 !"/>
       <CommentFilter comment="야 씨~ 너 죽을래? 진짜 ! 콱! 마! "/>
      </div>
    )
  }
}

export default App;

코드 수정후 제대로 동작하는 댓글 필터링 기능

728x90