🖥 논리곱 연산자를 이용한 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를 넣어야한다.
루프 안의 요소를 관리할 때 각 요소를 데이터와 매칭시켜주는 역할을 하여 리소스 비용을 줄인다.
🖨 실행 화면
🖨 버튼을 눌렀을 때
🔷 요구사항 예제를 만족시키면서 상태와 이벤트 바인딩에 대해 공부한다.
🖥 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;
const [count, setCount] = useState(0);
useState로부터 비구조화 할당을 이용해 상태와 이벤트에 따라 상태를 변화시키는 함수를 가져온다.
이를 함수 컴포넌트에서 훅(Hook)이라고 부른다. 함수 내에 있는 상태 관리를 위한 훅은 다양하다.
이벤트 핸들링 순서
1) 이벤트가 발생하면 상태를 변화시킬 함수를 정의한다.
2) 버튼 등에 이벤트를 바인딩한다. (onClick, onInput 등 다양하다)
부모 컴포넌트에게 메시지 전달하기
부모 컴포넌트에게 메시지를 전달하는 방법은 이 외에도 다양하다.
🖥 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로 넘긴다.
그렇기 때문에 자식 컴포넌트끼리는 서로의 상태 변화에 영향을 받지 않는다.
🖨 실행 화면
자식들끼리의 상태변화는 서로 영향을 받지 않음을 확인할 수 있다.
🔷 무언가 변화가 있을 때 감지하여 반응하는 훅
💡 두 번째 파라미터로 받는 배열을 비워두면 웹 페이지 실행과 동시에 작동하여 초기 이벤트 작성이나 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
🤷♂️ 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;
🖨 실행 화면
🖥 컴포넌트를 이용한 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;
🖨 실행 화면
🖥 컴포넌트를 이용한 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;
🖨 실행 화면
🔷 게시판과 같은 리스트를 보여주는 ui에서 많이 보여주는 사례
🖥 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번을 누르면?
정상 작동한다.
많은 것을 배운만큼 잘 간직하고 있어야겠다.
계속 반복하는 수 밖에...