도리쓰에러쓰

[React] Tab 만들기와 리액트에서의 등장 애니메이션 본문

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

[React] Tab 만들기와 리액트에서의 등장 애니메이션

강도리 2022. 1. 24. 23:44

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

 

[React] Component가 많을 땐 Context API 사용하기

저번 게시물에 이어서 작성하겠습니다. (코드 참고) [React] Component 3개 중첩하여 만들고 state 전달하기 저번 게시물에 이어서 작성하겠습니다. (코드 참고) [React] React에서의 Ajax 요청 방법 / axios 설

dori-coding.tistory.com


1. Tab 기능 만들기

- Tab 기능을 아래의 방법으로 구현할 예정입니다.

 

1) UI 만드는 법

1️⃣ UI 상태를 true / false state로 저장합니다.

2️⃣ state에 따라 UI 보이게 안보이게 합니다.

 

2) Tab UI 만드는 법

1️⃣ 몇번째 버튼 눌렀는지를 state로 저장합니다.

2️⃣ state에 따라 UI 보이게 안보이게 합니다.


이제 Tab 기능을 구현해보겠습니다.

 

1️⃣ 우선 Detail.js에 react-bootstrap을 import합니다.

import { Nav } from 'react-bootstrap';

 

 

2️⃣ 'react-bootstrap' component에 나와있는 nav를 복사 붙여넣기 합니다.

<Nav variant="tabs" defaultActiveKey="/home">
  <Nav.Item>
    <Nav.Link href="/home">Active</Nav.Link>
  </Nav.Item>
  <Nav.Item>
    <Nav.Link eventKey="link-1">Option 2</Nav.Link>
  </Nav.Item>
  <Nav.Item>
    <Nav.Link eventKey="disabled" disabled>
      Disabled
    </Nav.Link>
  </Nav.Item>
</Nav>

 

3️⃣ Nav 내용을 아래 코드와 같이 수정합니다.

<Nav className="mt-5" variant="tabs" defaultActiveKey="link-0">
    <Nav.Item>
        <Nav.Link eventKey="link-0">0번째 제목</Nav.Link>
    </Nav.Item>
    <Nav.Item>
        <Nav.Link eventKey="link-1">1번째 제목</Nav.Link>
    </Nav.Item>
</Nav>

- className="mt-5" : bootstrap에서 제공하는 class로 margin-top을 5로 설정

- defaultActiveKey : 링크 방문시 기본으로 눌러져 있는 버튼 설정

 

 

4️⃣ 몇번째 버튼 눌렀는지 저장할 state 데이터를 만듭니다.

let [pushTab, setPushTab] = useState(0);

 

 

5️⃣ 버튼을 누르면 state 변경 함수를 통해 state값이 변경되게 합니다.

<Nav className="mt-5" variant="tabs" defaultActiveKey="link-0">
    <Nav.Item>
        <Nav.Link eventKey="link-0" onClick={ ()=>{ setPushTab(0) } }>0번째 제목</Nav.Link>
    </Nav.Item>
    <Nav.Item>
        <Nav.Link eventKey="link-1" onClick={ ()=>{ setPushTab(1) } }>1번째 제목</Nav.Link>
    </Nav.Item>
</Nav>

 

 

6️⃣ Component를 하나 만들고 props로 변수 pushTab를 보냅니다.

<TabContent pushTab={pushTab} />

 

 

7️⃣ Component를 생성하여 조건문을 만들어줍니다.

function TabContent(props) {
    if(props.pushTab === 0) {
        return <div>0번째 내용</div>
    } else if(props.pushTab === 1) {
        return <div>1번째 내용</div>
    } else if(props.pushTab === 2) {
        return <div>2번째 내용</div>
    }
}

 

- 다음과 같이 Tab이 구현된 것을 확인할 수 있습니다.

'0번째 제목' tab을 클릭했을 때
'1번째 제목' tab을 클릭했을 때


2. 등장 애니메이션 추가하기

✨ 애니메이션 추가하는 방법

(1) 미리 애니메이션 주는 class 제작해놓고, 컴포넌트 등장 / 업데이트될 때 className에 부착

(2) 'react-transition-group' 라이브러리 사용

 

 

저는 (2)번의 방법으로 애니메이션을 추가하겠습니다.


1️⃣ 'react-transition-group' 라이브러리를 설치합니다.

  터미널에 npm install react-transition-group 혹은

  yarn이 설치되어 있으신 분들은 yarn add react-transition-group를 입력하여 설치합니다.

 

 

2️⃣ Detail.js에 'react-transition-group' 라이브러리를 import 합니다.

  🔔 'react-transition-group' 라이브러리는 Component 등장 / 업데이트 시 transition을 쉽게 줄 수 있습니다.

import { CSSTransition } from "react-transition-group";

 

 

3️⃣ <CSSTransition>으로 애니메이션 필요한 HTML 감쌉니다.

<CSSTransition>
    <TabContent pushTab={pushTab} />
</CSSTransition>

 

 

4️⃣ in, classNames, timeout 속성을 추가합니다.

<CSSTransition in={ switchBtn } classNames="wow" timeout={500}>
    <TabContent pushTab={ pushTab } />
</CSSTransition>

- in : 애니메이션 동작 스위치 ( switchBtn state 추후에 추가 예정 )

- classNames : class명

- timeout : 애니메이션 동작하는 시간 (단위 : 밀리세컨드)

 

 

5️⃣ Detail.scss에서 class로 애니메이션을 넣습니다.

.wow-enter {
    opacity: 0;
}

.wow-enter-active {
    opacity: 1;
    transition: all 500ms;
}

- 클래스명-enter : 애니메이션 시작 때 적용할 CSS

- 클래스명-enter-active : 애니메이션 동작 때 적용할 CSS

 

 

6️⃣ <CSSTransition>의 속성 in을 스위치하기 위해 state를 생성합니다.

let [switchBtn, setSwitchBtn] = useState(false);

 

 

 

7️⃣ TabContent Component에 setSwitchBtn()을 props로 전송합니다.

<CSSTransition in={ switchBtn } classNames="wow" timeout={500}>
    <TabContent pushTab={ pushTab } setSwitchBtn={ setSwitchBtn }/>
</CSSTransition>

 

 

8️⃣ Component가 load될 때 switchBtn state를 변경해주기 위해 useEffect()를 사용합니다.

function TabContent(props) {

    useEffect(()=>{
        props.setSwitchBtn(true);
    });

    if(props.pushTab === 0) {
        return <div>0번째 내용</div>
    } else if(props.pushTab === 1) {
        return <div>1번째 내용</div>
    } else if(props.pushTab === 2) {
        return <div>2번째 내용</div>
    }
}

 

 

9️⃣ Nav 버튼을 클릭하면 switchBtn을 false로 변경하여 스위치를 끕니다.

<Nav className="mt-5" variant="tabs" defaultActiveKey="link-0">
    <Nav.Item>
        <Nav.Link eventKey="link-0" onClick={ ()=>{ setSwitchBtn(false); setPushTab(0) } }>0번째 제목</Nav.Link>
    </Nav.Item>
    <Nav.Item>
        <Nav.Link eventKey="link-1" onClick={ ()=>{ setSwitchBtn(false); setPushTab(1) } }>1번째 제목</Nav.Link>
    </Nav.Item>
</Nav>

 

- '0번째 제목' tab을 클릭하면 '0번째 내용'이, '1번째 제목' tab을 클릭하면 '1번째 내용'이 서서히 나타나는 것을 확인할 수 있습니다.


3. 전체 코드

App.js

/* eslint-disable */
import React, { useState, useContext } 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';

export let inventoryContext = React.createContext();

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'>

            <inventoryContext.Provider value={inventory}>
              <div className='row'>
                {
                  products.map((a, i) => {
                    return <Card products={a} num={i} key={i}/>
                  })
                }
              </div>
            </inventoryContext.Provider>

            <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">
          <inventoryContext.Provider value={inventory}>
            <Detail products={ products } inventory={ inventory } setInventory={ setInventory } />
          </inventoryContext.Provider>
        </Route>
        
        <Route path="/:id">
            <div>아무말</div>
        </Route>
      </Switch>
    </div>
  );
}

function Card(props) {

  let inventory = useContext(inventoryContext);

  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>
      <p>재고 : { inventory[props.num] }</p>
    </div>
  )
}

export default App;

 

 

Detail.js

import React, { useContext, useEffect, useState } from 'react';
import { Nav } from 'react-bootstrap';
import { useHistory, useParams } from 'react-router-dom';
import styled from 'styled-components';
import { inventoryContext } from './App.js';
import './Detail.scss';

import { CSSTransition } from "react-transition-group";

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('');

    let [pushTab, setPushTab] = useState(0);
    let [switchBtn, setSwitchBtn] = useState(false);

    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);
    }

    let inventory = useContext(inventoryContext);
    
    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>

            <Nav className="mt-5" variant="tabs" defaultActiveKey="link-0">
                <Nav.Item>
                    <Nav.Link eventKey="link-0" onClick={ ()=>{ setSwitchBtn(false); setPushTab(0) } }>0번째 제목</Nav.Link>
                </Nav.Item>
                <Nav.Item>
                    <Nav.Link eventKey="link-1" onClick={ ()=>{ setSwitchBtn(false); setPushTab(1) } }>1번째 제목</Nav.Link>
                </Nav.Item>
            </Nav>
            
            <CSSTransition in={ switchBtn } classNames="wow" timeout={500}>
                <TabContent pushTab={ pushTab } setSwitchBtn={ setSwitchBtn }/>
            </CSSTransition>

        </div>
    )
}

function TabContent(props) {

    useEffect(()=>{
        props.setSwitchBtn(true);
    });

    if(props.pushTab === 0) {
        return <div>0번째 내용</div>
    } else if(props.pushTab === 1) {
        return <div>1번째 내용</div>
    } else if(props.pushTab === 2) {
        return <div>2번째 내용</div>
    }
}

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

export default Detail;

 

 

Detail.scss

$mainColor : #ff0000;

.wow-enter {
    opacity: 0;
}

.wow-enter-active {
    opacity: 1;
    transition: all 500ms;
}

.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;
}

 

* 이외의 코드는 이전 게시물에 작성된 코드와 일치합니다.

Comments