도리쓰에러쓰

[React] Component 3개 중첩하여 만들고 state 전달하기 본문

코딩애플 (React)/기초수업 (코딩애플) - 2

[React] Component 3개 중첩하여 만들고 state 전달하기

강도리 2022. 1. 20. 22:50

저번 게시물에 이어서 작성하겠습니다. (코드 참고)

 

[React] React에서의 Ajax 요청 방법 / axios 설치

저번 게시물에 이어서 작성하겠습니다. (코드 참고) [React] Lifecycle Hook(예전 문법) vs useEffect(요즘 문법) 저번 게시물에 이어서 작성하겠습니다. (코드 참고) [React] SASS 개념, 설치 및 기본 문법 저번

dori-coding.tistory.com


1. 3개가 중첩되어 있는 Component에 데이터바인딩

- 우선 App.js에 재고량 데이터를 생성합니다.

let [inventory, setInventory] = useState([10, 11, 12]);

 

- Detail.js에서 <p>재고 : ??? </p>를 생성합니다.

<div className='row'>
    <div className='col-md-6'>
        <img className='img' src={ process.env.PUBLIC_URL + '/images/img'+ (Number(findProduct.id) + 1)+'.jpg' } />
    </div>
    <div className='col-md-6 mt-4'>
        <h4 className='pt-5'>{ findProduct.title }</h4>
        <p>{ findProduct.content }</p>
        <p>{ findProduct.price }</p>
        <p> 재고 : ???</p>
        <button className='btn btn-danger'>주문하기</button>
        <br />
        <br />
        <button className='btn btn-danger' onClick={ () => {
            history.goBack();
        }}>뒤로가기</button>
    </div>
</div>

 

- 위의 코드에서 <p>재고 : ???</p> 라는 부분을 새로운 Component에 넣을 예정입니다.

function Info() {
    return(
        <p> 재고 : ???</p>
    )
}

 

- 그리고 원래 <p>재고 : ???</p>가 있었던 부분을 Component로 대체하였습니다.

<div className='row'>
    <div className='col-md-6'>
        <img className='img' src={ process.env.PUBLIC_URL + '/images/img'+ (Number(findProduct.id) + 1)+'.jpg' } />
    </div>
    <div className='col-md-6 mt-4'>
        <h4 className='pt-5'>{ findProduct.title }</h4>
        <p>{ findProduct.content }</p>
        <p>{ findProduct.price }</p>
        <Info />
        <button className='btn btn-danger'>주문하기</button>
        <br />
        <br />
        <button className='btn btn-danger' onClick={ () => {
            history.goBack();
        }}>뒤로가기</button>
    </div>
</div>

 

- 지금 현재 Component가 App Component 안에 Detail Component가 있고,

  Detail Component 안에 Info Component가 있는데

  App Component에서 Info Component로 데이터 바인딩을 해보겠습니다.

 

- 우선 App Component에서 Detail Component로 데이터를 보내려면, App Component 안에 Detail Component 코드가 적혀있는 부분을 찾아 props를 작성해주시면 됩니다. (App.js)

<Detail products={ products } inventory={ inventory }/>

 

- 현재 데이터가 Detail Component까지 왔으니, Detail Component에서 Info Component로 데이터를 보내면 됩니다. 그러기 위해서 Detail Component 안에 Info Component 코드가 적혀있는 부분을 찾아 props를 작성해주시면 됩니다. (Detail.js)

<Info id={ findProduct.id } inventory={ props.inventory }> </Info>

 

- 최종적으로 Info Component에서 데이터바인딩을 해주시면 됩니다.

function Info(props) {
    return(
        <p> 재고 : { props.inventory[props.id] }</p>
    )
}

- 그럼 아래의 화면과 같이 재고량이 출력된 것을 확인할 수 있습니다.


2. [주문하기] 버튼을 클릭하면 재고량 -1이 되려면?

 

- 아래 코드처럼 [주문하기] 버튼 이벤트를 생성하여 setInventory()를 호출하면 될까요?

<button className='btn btn-danger' onClick={ () => { setInventory() } }>주문하기</button>

  * 위 코드처럼 setInventory() 함수를 호출할 수 없습니다. 왜냐하면, setInventory() 함수는 부모 Component인 <App />이 가지고 있기 때문입니다. inventory state를 변경하려면 props를 통해 전송하면 됩니다.

 

- App.js에서 Detail.js로 setInventory()를 전송합니다.

<Detail products={ products } inventory={ inventory } setInventory={ setInventory } />

 

- [주문하기] 버튼 이벤트에 inventoryOut()이라는 함수를 호출합니다.

<button className='btn btn-danger' onClick={ () => { inventoryOut() } }>주문하기</button>

 

- 아래 코드와 같이 state를 조작하여 재고량 -1를 하면 됩니다.

function inventoryOut() {
    let id = findProduct.id;
    let copyArr = [...props.inventory];

    copyArr[id] = copyArr[id] - 1;
    props.setInventory(copyArr);
}
* state 조작하는 방법
 1) state의 사본 만들기
 2) 사본 변경
 3) 사본을 변경함수에 넣기

- 다음과 같이 [주문하기] 버튼을 누르면 재고량이 줄어드는 것을 확인할 수 있습니다.

 

< 오늘의 결론 >

1. 하위 컴포넌트가 몇개든 데이터를 전송하려면 props를 씁니다.

   ( 부모 컴포넌트 -> 자식 컴포넌트 -> 그 자식 컴포넌트 ··· )

2. 하위 컴포넌트가 상위 컴포넌트 state변경하려면 state 변경함수 사용합니다.

   그게 상위 컴포넌트에 있으면 props로 전송하여 사용하면 됩니다.


3. 전체 코드

(shop 프로젝트의 모든 코드입니다.)

 

App.js

/* eslint-disable */
import React, { useState } from 'react'
import { Navbar, Container, Nav, NavDropdown, Button } from 'react-bootstrap';
import './App.css';
import data from './data.js';
import Detail from './Detail.js';
import axios from 'axios';

import { Link, Route, Switch } from 'react-router-dom';

function App() {

  let [products, setProducts] = useState(data);
  let [inventory, setInventory] = useState([10, 11, 12]);

  return (
    <div className="App">
      <Navbar bg="light" expand="lg">
        <Container>
          <Navbar.Brand href="#home">Saint Laurent</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="me-auto">
              <Nav.Link as={Link} to="/">Home</Nav.Link>
              <Nav.Link as={Link} to="/detail">Detail</Nav.Link>
              <NavDropdown title="Dropdown" id="basic-nav-dropdown">
                <NavDropdown.Item href="#action/3.1">Action</NavDropdown.Item>
                <NavDropdown.Item href="#action/3.2">Another action</NavDropdown.Item>
                <NavDropdown.Item href="#action/3.3">Something</NavDropdown.Item>
                <NavDropdown.Divider />
                <NavDropdown.Item href="#action/3.4">Separated link</NavDropdown.Item>
              </NavDropdown>
            </Nav>
          </Navbar.Collapse>
        </Container>
      </Navbar>

      <Switch>
        <Route exact path="/">
          <div className='Jumbotron'>
            <h1>20% Season Off</h1>
            <p>
              This is a simple hero unit, a simple jumbotron-style component for calling
              extra attention to featured content or information.
            </p>
            <p>
              <Button variant="primary">Learn more</Button>
            </p>
          </div>

          <div className='container'>
            <div className='row'>
              {
                products.map((a, i) => {
                  return <Card products={a} num={i} key={i}/>
                })
              }
            </div>
            <button className='btn btn-primary' onClick={()=>{
              axios.get('https://codingapple1.github.io/shop/data2.json')
              .then((result)=>{
                setProducts([...products, ...result.data]) 
              })
              .catch(()=>{
                console.log('fail');
              })
            }}>더보기</button>
          </div>
        </Route>
        
        <Route path="/detail/:id">
          <Detail products={ products } inventory={ inventory } setInventory={ setInventory } />
        </Route>
        
        <Route path="/:id">
            <div>아무말</div>
        </Route>
      </Switch>
    </div>
  );
}

function Card(props) {
  return(
    <div className='col-md-4'>
      <img className="img" src={ 'images/img'+ (props.num + 1) +'.jpg' } />
      <h4>{ props.products.title }</h4>
      <p>{ props.products.content }</p>
      <p>{ props.products.price }</p>
    </div>
  )
}

export default App;

 

App.css

.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.Jumbotron {
  background-color: #EBECF0;
  height: 200px;
  padding-top: 25px;
  background-image: url('./background.jpg');
  background-size: cover;
  color: #FAFAFA;
}

.img {
  position: relative;
  width: 80%;
  height: 300px;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

 

data.js

export default [
    {
        id : 0,
        title : "Black Jacket",
        content : "Born in Paris",
        price : 6000000
    },

    {
        id : 1,
        title : "White Bag",
        content : "Born in Paris",
        price : 5000000
    },

    {
        id : 2,
        title : "Black Bag",
        content : "Born in Paris",
        price : 4000000
    },
]

 

Detail.js

import React, { useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import styled from 'styled-components';
import './Detail.scss';

let Box = styled.div`
    padding : 20px;
`;

let Title = styled.h4`
    font-size : 25px;
    color : ${ props => props.color }
`;

function Detail(props) {

    let { id } = useParams();
    let history = useHistory();
    let findProduct = props.products.find(function(product) {
        return product.id = id;
    });

    let [alert, setAlert] = useState(true);
    let [inputData, setInputData] = useState('');

    useEffect(()=>{
        let timer = setTimeout(() => { setAlert(false) }, 2000);
        return ()=>{ clearTimeout(timer) }
    }, []);

    function inventoryOut() {
        let id = findProduct.id;
        let copyArr = [...props.inventory];
        
        copyArr[id] = copyArr[id] - 1;
        props.setInventory(copyArr);
    }

    return(
        <div className='container'>
            <Box>
                <Title className='red'>상세페이지</Title>
            </Box>

            <input onChange={ (e)=>{ setInputData(e.target.value) } }/>

            {
                alert === true
                ? (<div className='my-alert-red'>
                        <p>재고가 얼마 남지 않았습니다!</p>
                    </div>)
                : null
            }
            
            <div className='row'>
                <div className='col-md-6'>
                    <img className='img' src={ process.env.PUBLIC_URL + '/images/img'+ (Number(findProduct.id) + 1)+'.jpg' } />
                </div>
                <div className='col-md-6 mt-4'>
                    <h4 className='pt-5'>{ findProduct.title }</h4>
                    <p>{ findProduct.content }</p>
                    <p>{ findProduct.price }</p>
                    <Info id={ findProduct.id } inventory={ props.inventory }> </Info>
                    <button className='btn btn-danger' onClick={ () => { inventoryOut() } }>주문하기</button>
                    <br />
                    <br />
                    <button className='btn btn-danger' onClick={ () => {
                        history.goBack();
                    }}>뒤로가기</button>
                </div>
            </div>
        </div>
    )
}

function Info(props) {
    return(
        <p> 재고 : { props.inventory[props.id] }</p>
    )
}

export default Detail;

 

Detail.scss

$mainColor : #ff0000;

.red {
    color: $mainColor;
}

@mixin func() {
    background: #eeeeee;
    padding: 20px;
    border-radius: 5px;
    max-width: 500px;
    width: 100%;
    margin: auto;
    p {
        margin-bottom: 0;
    }
}

.my-alert {
    @include func();
}

.my-alert-red {
    @extend .my-alert;
    background: #fa8072;
}

 

index.html에서 <head> 부분에 bootstrap 코드 적용

<!-- BootStrap -->
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
  integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
  crossorigin="anonymous"
/>

 

Comments