react.js 사용하기 Ⅰ

young-gue Park·2023년 3월 6일
0

React

목록 보기
3/17
post-thumbnail

⚡ react.js 사용하기 Ⅰ


📌 분기와 반복

  1. 컴포넌트는 JSX를 반환하는 함수
  2. 함수 내에서 JSX를 처리하는 과정에서 분기와 반복을 사용할 수 있다.
  3. JSX 내에서 if와 for와 같은 문법 사용이 어렵기 때문에 편의를 위해서 표현식인 삼항연산자와 map, filter 등을 사용한다.

🖥 논리곱 연산자를 이용한 JSX 렌더링 여부 결정

import './App.css';
import {useState} from "react";

function App() {
  const [visible, setVisible] = useState(false);

  return (
    <div className="App">
      <button onClick={() => setVisible(!visible)}>Toggle</button>
      {visible &&(<h1>논리곱 연산자를 통해 쉽게 JSX 렌더링 여부를 결정할 수 있습니다.</h1>)}
    </div>
  );
}

export default App;

🖥 삼항 연산자를 이용한 JSX 렌더링 여부 결정

import './App.css';
import {useState} from "react";

function App() {
  const [visible, setVisible] = useState(false);

  return (
    <div className="App">
      <button onClick={() => setVisible(!visible)}>Toggle</button>
      {visible ? (<h1>삼항 연산자를 통해 쉽게 JSX 렌더링 여부를 결정할 수 있습니다.</h1>) : null}
    </div>
  );
}

export default App;

🖨 실행 화면

🖨 버튼을 눌렀을 때

🖥 삼항 연산자를 이용한 JSX 렌더링 여부 결정 2 (App.js)

import './App.css';
import {useState} from "react";
import Board from './components/Board';

function App() {
  const [visible, setVisible] = useState(false);

  const articles = [
    {
      id:1,
      title: '안녕!',
      author: '박영규'
    },
    {
      id:2,
      title: '그래 안녕!',
      author: '박일규'
    },
    {
      id:3,
      title: '너도 안녕!',
      author: '박이규'
    }
]
  return (
    <div className="App">
      <button onClick={() => setVisible(!visible)}>Toggle</button>
      {visible ? <Board articles = {articles}></Board> : <p>게시판을 보려면 버튼을 클릭하시오.</p>}
    </div>
  );
}

export default App;

🖥 map을 이용한 JSX 렌더링 루프 (Board.js)

import PropTypes from 'prop-types';

const Board = ({articles}) => {
    return (
        <div>
            <h1>Board</h1>

            <ul>
                {articles.map(article => (
                    <li key={article.id}> // !! key 넣기
                        {article.id} | {article.title} | {article.author}
                    </li>
                ))}
            </ul>
        </div>
    )
}

Board.propTypes = {
    articles: PropTypes.array
}

export default Board;

JSX 내에서 루프를 돌린다면 반드시 가장 상위의 요소에 key를 넣어야한다.
루프 안의 요소를 관리할 때 각 요소를 데이터와 매칭시켜주는 역할을 하여 리소스 비용을 줄인다.

🖨 실행 화면

🖨 버튼을 눌렀을 때


📌 상태와 이벤트 바인딩

🔷 요구사항 예제를 만족시키면서 상태와 이벤트 바인딩에 대해 공부한다.

⭐ 요구사항

  1. Counter 컴포넌트 구현하기
  2. 모든 Counter 컴포넌트의 합 구하기

⭐ 구현

🖥 Counter/index.js

// Counter 컴포넌트의 기능
// 1. 증감되는 기능
// 2. 부모 컴포넌트에게 메시지를 전달하는 기능
import { useState } from "react";
import PropTypes from 'prop-types';

function Counter({ onIncrease, onDecrease }) {
    const [count, setCount] = useState(0);

    const handleIncrease = () => {
        setCount(count +1);
        if(onIncrease) {
            onIncrease(count+1);
        }
    }

    const handleDecrease = () => {
        setCount(count -1);
        if(onDecrease) {
            onDecrease(count-1);
        }
    }

    return (
        <div>
            <span style = {{fontSize:40}}>{count}</span>
            <br/>
            <button onClick ={handleIncrease}>+</button>
            <button onClick ={handleDecrease}>-</button>
        </div>
    )
}

Counter.propTypes = {
    onIncrease: PropTypes.func,
    onDecrease: PropTypes.func
}

export default Counter;
  1. 분기 파트에서도 언급했던 useState를 이용하여 지역 상태를 관리할 수 있다.
const [count, setCount] = useState(0);

useState로부터 비구조화 할당을 이용해 상태와 이벤트에 따라 상태를 변화시키는 함수를 가져온다.
이를 함수 컴포넌트에서 훅(Hook)이라고 부른다. 함수 내에 있는 상태 관리를 위한 훅은 다양하다.

  1. 이벤트 핸들링 순서
    1) 이벤트가 발생하면 상태를 변화시킬 함수를 정의한다.
    2) 버튼 등에 이벤트를 바인딩한다. (onClick, onInput 등 다양하다)

  2. 부모 컴포넌트에게 메시지 전달하기

  • 부모 컴포넌트에서 props를 통해 메시지를 받을 수 있도록 함수를 전달하는 방식으로 구현되었다.

    부모 컴포넌트에게 메시지를 전달하는 방법은 이 외에도 다양하다.

🖥 App.js

import './App.css';
import { useState } from "react";
import Counter from './components/Counter';

function App() {
 const [totalCount, setTotalCount] = useState(0);

  return (
    <div className="App">
      TotalCount: {totalCount}
      <Counter 
      onIncrease={() => {setTotalCount(totalCount + 1);}}
      onDecrease={() => {setTotalCount(totalCount - 1);}}
      />
      <Counter 
      onIncrease={() => {setTotalCount(totalCount + 1);}}
      onDecrease={() => {setTotalCount(totalCount - 1);}}
      />
      <Counter 
      onIncrease={() => {setTotalCount(totalCount + 1);}}
      onDecrease={() => {setTotalCount(totalCount - 1);}}
      />
    </div>
  );
}

export default App;

onIncrease와 onDecrease를 자식 컴포넌트들이 따로 선언하면서 부모에게 props로 넘긴다.
그렇기 때문에 자식 컴포넌트끼리는 서로의 상태 변화에 영향을 받지 않는다.

🖨 실행 화면

자식들끼리의 상태변화는 서로 영향을 받지 않음을 확인할 수 있다.


📌 useEffect

🔷 무언가 변화가 있을 때 감지하여 반응하는 훅

  • 첫 번째 파라미터에는 반응하는 부분, 두 번째 파라미터는 어떤 것을 감지할지 배열로 받는다.
  • 라이프 사이클처럼 사용할 수 있다.

💡 두 번째 파라미터로 받는 배열을 비워두면 웹 페이지 실행과 동시에 작동하여 초기 이벤트 작성이나 API호출 등의 행위를 하고 컴포넌트가 제거될 때 아예 종료되거나 다른 함수가 실행되게끔 할 수 있다. 이를 라이프 사이클처럼 사용할 수 있다.

🖥 useEffect 사용 예시(App.js)

import './App.css';
import { useEffect, useState } from "react";

function App() {
 const [Count, setCount] = useState(0);

 useEffect(() => {
  console.log(`${Count}번 눌림.`);
 }, [Count]); // count의 변화를 감지

 useEffect(() => {
  console.log(`컴포넌트 실행`);
  const handleScroll = () => {
    console.log(window.scrollY)
  }; 
  document.addEventListener("scroll", handleScroll); // 전역적인 이벤트를 사용할 때 쓸 수 있다.
  return () => document.removeEventListener('scroll', handleScroll); // return으로 반환한 함수는 컴포넌트가 제거될 때 실행된다. 
 }, []); // 컴포넌트가 처음 로드될 때 실행

  return (
    <div>
      <div>{Count}번 누르셨습니다.</div>
      <button onClick={() => setCount(Count+1)}>+</button>

      <div style={{height:10000}}></div>
    </div>
  );
}

export default App;

🖨 실행 화면

🖨 버튼을 3번 눌렀을 때

🖨 하단으로 스크롤 시 log


📌 useRef

  1. DOM에 직접 접근할 때 사용한다.
  2. 혹은 지역 변수로 사용할 때 사용한다.

🤷‍♂️ useState와의 차이가 있나요?
useState는 값이 변경될 때 다시 렌더링을 한다.
반면 useRef는 값이 변경되더라도 다시 렌더링을 하지 않는다.

🖥 useRef DOM 직접 접근 사용 예시(App.js)

import './App.css';
import { useRef } from "react";

function App() {
 const inputRef = useRef();

  return (
    <div>
      Input: <input ref={inputRef}/>
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </div> // DOM에 직접 접근하기
  );
}

export default App;

🖨 실행 화면

  • Focus를 누르면 Input창으로 커서가 포커싱된다.

🖥 컴포넌트를 이용한 useRef 지역 변수 사용 예시(input.js)

import React, { createRef, useEffect } from "react";

const Input = React.forwardRef((_, ref) => {

    useEffect(() => {
        console.log(ref.current)
    }, [createRef])

    return (
        <>
            Input: <input ref={ref}/>
        </>
    )
})

export default Input;

💡 forwardRef
forwardRef를 사용하면 부모 컴포넌트에서 하위 컴포넌트로 ref를 전달할 수 있다. 이렇게 전달받은 ref를 HTML 요소의 속성으로 넘겨줌으로써 함수 컴포넌트가 ref를 통한 제어가 가능해진다.

(❗❗❗ 함수 컴포넌트는 인스턴스가 없다. 즉, ref가 애초에 존재하지 않는다. 그래서 forwardRef를 사용하는 것.)

🖥 컴포넌트를 이용한 useRef 지역 변수 사용 예시(App.js)

import './App.css';
import { useRef } from "react";
import Input from './components/input';

function App() {
 const inputRef = useRef(); // useRef를 이용하여 inputRef에 값을 넣는다

  return (
    <div>
      <Input ref={inputRef}/>
      <button onClick={() => inputRef.current.focus()}>Focus</button>
    </div> 
  );
}

export default App;

🖨 실행 화면

  • Focus를 누르면 Input창으로 커서가 포커싱된다.
  • Focus를 아무리 눌러도 useEffect는 처음 작동 이후 다시 작동하지 않는다.

🖥 컴포넌트를 이용한 useRef 지역 변수 사용 예시2 (AutoCounter.js)

import {useRef, useState} from "react";

const AutoCounter = () => {
    const [count, setCount] = useState(0);
    const interValId = useRef(); // useRef를 이용하여 interValId에 값을 넣는다

    const handleStart = () => {
        interValId.current = setInterval(() => {
            setCount((count) => count + 1);
        }, 1000); // id가 변화하여도 다시 렌더링하지 않는다.
    }

    const handleStop = () => {
        clearInterval(interValId.current)
    }

    return (
        <>
            <div>{count}</div>
            <button onClick={handleStart}>start</button>
            <button onClick={handleStop}>stop</button>
        </>
    )
}

export default AutoCounter;

🖥 컴포넌트를 이용한 useRef 지역 변수 사용 예시(App.js)

import './App.css';
import { useRef } from "react";
import Input from './components/input';
import AutoCounter from './components/AutoCounter';

function App() {
 const inputRef = useRef();

  return (
    <div>
      <Input ref={inputRef}/>
      <button onClick={() => inputRef.current.focus()}>Focus</button>
      <AutoCounter></AutoCounter>
    </div> 
  );
}

export default App;

🖨 실행 화면

  • start 버튼을 누르면 1초마다 count가 1씩 증가하고 stop을 누르면 이내 멈춘다.
  • 그럼에도 다시 렌더링 되는 경우는 없다.

📌 페이지네이션

🔷 게시판과 같은 리스트를 보여주는 ui에서 많이 보여주는 사례

  • 오늘은 예외를 처리하기 위한 분기들과 루프, 이벤트 바인딩을 이용하여 간단하게 제작한다.
  • 글을 10개 단위로 끊어 페이지에 출력시키며 이전이나 다음, 직접 숫자 버튼을 클릭하여 해당하는 번호의 게시물을 보여준다.
  • 포커싱 된 인덱스의 버튼은 붉은 색으로 표시된다.
  • 숫자 버튼은 5개로 고정되며 5개의 숫자 버튼은 페이지 수가 5개를 넘어가면 다른 인덱스의 페이지로 이동했을 때 자동으로 그 숫자를 중심으로 양 옆의 두 페이지 버튼으로 바뀐다.

🖥 components/Board.js

import PropTypes from 'prop-types';

const Board = ({articles}) => {
    return (
        <div>
            <h1>Board</h1>

            <ul>
                {articles.map(article => (
                    <li key={article.id}>
                        {article.id} | {article.title}
                    </li>
                ))}
            </ul>
        </div>
    )
}

Board.propTypes = {
    articles: PropTypes.array
}

export default Board;

🖥 components/Pagination.js

import { useState } from "react";

const Pagination = ({ defaultPage, limit, total, onChange }) => {
    const [page, setPage] = useState(defaultPage);
    const totalPage = Math.ceil(total / limit);

    const handleChangePage = (newPage) => {
        onChange(newPage);
        setPage(newPage);
    };

    return (
        <div>
            <button onClick={() => page !== 0 && handleChangePage(page -1)}>이전</button>
            {Array.from(new Array(totalPage), (_, i) => i)
            .filter(i => {
                if(page < 3) {
                    return i < 5;
                } else if (page > totalPage - 3) {
                    return i >= totalPage - 5;
                }
                return i >= page-2 && i <=page+2
            })
            .map(i => (
                <button 
                key = {i} 
                style={{backgroundColor: page === i ? 'red' : undefined}}
                onClick={() => handleChangePage(i)}
                >{i + 1}</button>
            ))}
            <button onClick={() => page + 1 !== totalPage && handleChangePage(page +1)}>다음</button>
        </div>
    )
}

export default Pagination;

🖥 App.js

import { useState } from 'react';
import './App.css';
import Board from './components/Board';
import Pagination from './components/Pagination';

function App() {
  const [page,setPage] = useState(0);
const articles = new Array(100).fill().map((_, i) => ({
  id: i,
  title: `${i}번 게시물`
}))

  const limit = 10;
  const offset = page * limit;

  return (
    <div>
      <Pagination defaultPage={0} limit={limit} total={articles.length} onChange={setPage}></Pagination>
      <Board articles={articles.slice(offset, offset + limit)}/>
    </div> 
  );
}

export default App;

🖨 실행 화면

  • 처음 실행

  • 5번을 누르면?

  • 이전을 누르면?

정상 작동한다.


많은 것을 배운만큼 잘 간직하고 있어야겠다.
계속 반복하는 수 밖에...

profile
Hodie mihi, Cras tibi

0개의 댓글