
- 새로운 프로젝트를 생성해보자
npx create-react-app $작명npm startnpm을 사용해서 개발할것이다. 앱을 실행 시켜보자.- 페이지 디자인을 위한
bootstrap이라는 라이브러리를 설치해보자.
https://react-bootstrap.github.io/getting-started/introductionindex.html의head태그안에 link된 bootstrap 위의 정보를 넣어주자.- bootStrap 에서 원하는
UI를 복사 붙여넣기 해주자- 사용할
UI를 import 로 선언해주자. (태그 앞대문자 선언)import { Navbar,Container,Nav } from 'react-bootstrap';🤨 : 선언한
UI의 스타일 수정은 ClassName을 사용해 css변경![]()
- 대문사진을 하나 넣어보자.
div태그를 생성후classaname선언Img 폴더를 생성후 그안에 대문사진 이미지를 넣어주자.- css로 이동
background-image: url('')주소와 css 코드 입력- 상품컬럼 3개를 만들고 이미지주소를 통해 이미지를 붙이자. (링크를 참조)
https://react-bootstrap.netlify.app/layout/grid/#rb-docs-content- 이미지를 많이 쓸땐 Public에 관리하는것이 좋다 하지만 경로가 바뀌면 오류가 발생할수 있음으로 다음과 같은 방법으로 코드를 짜면된다.
<img src={process.env.PUBLIC_URL + '/img/logo.png'}
🤨 :html에서 이미지를 넣을땐 import 로 이름과 경로를 선언해주어야함
상품컬럼에 대한 정보를 서버를 통해 가져왔다 가정하고 진행하겠다.
1. useState를 사용let [art] = useState(data);
2. data.js 라는 상품컬럼 정보 에대한 파일을 만들어export해준다.
3. 다시 App.js로 돌아와서 data.js를import해준다.
4. data.js 의 구조는array안의object구조이기 때문에 선언할떄 유의한다.
5. 즉 첫번째 상품의 제목을 알고싶으면art[0].title이런식으로 써야한다.
컴포넌트 함수를 만들어서 원래의 코드를 더 간단하게 표현하자.
- 기존의
App함수가 아닌 외부에MidArt라는 컴포넌트 함수를 만들것이다.- 상품컬럼 3개가 동일한 형식이기 때문에 이를 컴포넌트로 대체한다.
- 컴포넌트 함수안에 동일한 형식인 상품컬럼 한가지의 코드 내용을 복붙 한다.
- 🚨문제 발생 외부의 함수에서
state변수 사용으로 코드가 동작하지 않는다.props를 사용하여 부모함수에서state변수에대한 정보를 가져오게 한다.props선언으로 인해 컴포넌트를 사용하는 부분에 다음과 같이 state 이름과 작명을 해주고<MidArt art={art[0]}></MidArt>컴포넌트 함수에선 state 사용 부분에props.art.title이와같이props.을 붙여준다.- 6번까지가
상품컬럼[0]에대한 컴포넌트 생성이고 나머지 두개도 진행해준다.App함수
Map을 사용해 3개의 위 반복코드를 더 간단하게 만들어줄것이다.
- map 함수는 반복문이며 map 앞의 변수의 크기만큼 반복한다.
art.map은 art의 배열수만큼 반복실행한다. 함수로 만들어보자.{art.map(() => { return<MidArt art={art[0]}></MidArt>; })}
- 위와 같은 코드는
art[0]상품컬럼을art배열의 개수인 3번만큼 반복한다.- 때문에 반복문이 돌때마다 1씩 증가하는 파라미터
i를 사용하여 해결한다.{art.map((a,i) => { return<MidArt art={art[i]}></MidArt>; })}
<div>들을 갈아치워서 보여주는데 react-router-dom은 이것을 간편하게 해준다.
- react-router-dom 설치 터미널 입력
npm install react-router-dom@6- index.js세팅 import 이후 BrowserRouter로 App을 감싼다.
import { BrowserRouter } from 'react-router-dom';root.render( <React.StrictMode> <BrowserRouter> <App/> </BrowserRouter> </React.StrictMode> );
- 이제 메인페이지와 어바웃페이지 2개를 만들어보자.
- 먼저 상단에서 여러가지 컴포넌트를 import 해
Routes안에Route를 작성- 그리고
path엔 경로element엔 보여줄 html을 작성하면된다.import {Routes,Route,Link} om 'react-router-dom' function App(){ return( (생략) <Routes> <Route path="/" element={<div>메인</div> }/> <Route path="/detail" element={<div>상세</div> }/> </Routes> ) }
- 이제
nav를 제외한 기존의view들을element메인자리 에 넣어주자.detail엔 Detail.js 파일에 컴포넌트 함수를 만들어element에 넣어주자.- 이제
<Link를 이용해서 메인과 상세 페이지를 이동할수있도록 하면 끝<Link to="/">홈</Link> <Link to="/Detail">상세페이지</Link>😃😃😃
path경로에'/'를 경로로 지정하면 초기 url의 경로가 나오며 만약
path경로에'*'를 경로로 지정하면 앞서 선언한 라우터들을 제외한 나머지 경로에 대한 페이지를 만들수있다 예를들어404페이지같은 에러페이지..<Route path="*" element={<div>없는페이지임</div>} />
- 새로운 기능 useNavigate 와 Outlet 을 import 하는것을 잊지말자
import{ Routes, Route, Link,useNavigate,Outlet} from 'react-router-dom'- Link태그와 같은 기능을 하지만 더 깔끔하게 함수를 사용가능한
useNavigate를 만들어보자. 먼저 변수를 만들어 선언해주고 사용되는 태그의event함수에navigate선언과경로를 지정해준다.navigate 안에-1을 넢으면 뒤로가기1을 넣으면 앞으로 가는등 여러가지 기능 활용이 가능하다.function App(){ let navigate = useNavigate(); return( ------------(생략) <button onClick={()=>{navigate('/Detail')}>상세페이지</button> <button onClick={()=>{navigate(-1)}>뒤로가기</button>) ;}- 서브경로를 만들수 있는
nested Routes를 사용해보자.
회사정보라는 페이지 안에회사멤버,회사위치라는 두개의 페이지를 만들자<Route path='/about" element{<About/>} > <Route path='member' element{<div>멤버들</div>}/> <Route path='location' element{<div>위치</div>}/> </Route>- 이렇게 생성하면 이제
'about'이란 경로 하위에'member','location'
이 생성되고 about 페이지를 공통으로 보여주며 member나 location의 페이지또한 보여줄수있다.이렇듯 Route 안에 Route를 넣는 방식을 nested
Route 라고 하며 상위 페이지를 고정시키고 하위 박스들만 변경할때 사용된다.
- 하지만 여기서 실제로
about/member위치로 가보면 멤버들 이라는 div가 안보이는데 이는 상위 경로에서 하위 route를 보여주는 위치를 지정하지 않았기 때문이다.때문에 about 컴포넌트로 돌아가Outlet이라는 태그를 선언해준다.funciton About() { return( <div> <h4>어바웃 페이지</h4> <Outlet></Outlet> </div> ) }
- props를 전송하는 방법은 동일하다 사용하는 컴포넌트의 위치에 아래처럼 작명
<Route path="/detail" element={ <Detail shoes={shoes}/> }/>
- 이후 Detail.js 로 돌아가 아래 코드처럼 사용해주면 props를 사용가능하다.
<h4 className="pt-5">{props.shoes[0].title}</h4> <p>{props.shoes[0].content}</p> <p>{props.shoes[0].price}원</p>
- 그럼 페이지를 여러개 만들면 어떻게 해야함? 아래처럼 무식한 방법으로 만듬??
<Route path="/detail/0" element={ <Detail shoes={shoes}/> }/> <Route path="/detail/1" element={ <Detail shoes={shoes}/> }/> <Route path="/detail/2" element={ <Detail shoes={shoes}/> }/>
- 저렇게 하지않기 위해 URL파라미터라는 문법을 사용 아래와 같은방법으로 path 에
/:id를 입력하면 말그대로 사용자가 url로 입력한detail/다음의 parm을 가져와 보여준다는 뜻<Route path="/detail/:id" element={ <Detail shoes={shoes}/> }/>
- 근데 여기서 문제발생 2번 처럼 shoes[0]의 자료만 하드코딩 해왔기때문에
3번출력하면 결국 3개의 같은 페이지만 생성된다. 어떻게 해결?- useParam 이라는 훅을 사용하면 url파라미터에 입력된 숫자를 가져올수있음
import { useParams } from 'react-router-dom'; (생략) let {id} = useParams(); return( (생략) <h4 className="pt-5">{props.shoes[id].title}</h4>
- 문제!! 근데 만약 상품의 순서가 바뀌면 어떻게 상세페이지를 보여줌???
1) 먼저 data.js에 상품명,타이틀 등과함께 구분지을수있는 id 값이 존재해야함
2) 이후 자바스크립트의 find()문법을 사용하여 배열의 오브젝트값인 id 를색출
3)array자료.find(()=>{ return 조건식 })이렇게 쓰면 조건식에 맞는 자료를 찾아서 이 자리에 남겨줌let { id } = useParams(); let 찾은상품 = props.shoes.find(function(x){ return x.id == id // let 찾은상품 = props.art.find((x) => x.id == id); }); return( (생략) <h4>{찾은상품.title}</h4> <p>{찾은상품.content}</p> <p>{찾은상품.price}원</p>
- find()는 array 뒤에 붙일 수 있으며 return 조건식 적으면 됨 그럼 조건식에 맞는 자료 남겨줌
- find() 콜백함수에 파라미터 넣으면 array자료에 있던 자료를 뜻함. x라고 작명해봤음
- x.id == id 라는 조건식을 써봄. 그럼 array자료.id == url에입력한번호 일 경우 결과를 변수에 담아줌
- 터미널 에서
npm install styled-components- 사용할 컴포넌트 에서
import styled from 'styled-components'- 사용법 상단에서 변수와 변수명을 생성후
styled.태그명을 붙여준다.- 이후 ` 백틱을 사용하여 열고닫아 내부에서 css처럼 스타일을 변형시켜준다.
let Box = styled.div` padding : 20px; color : grey `;
- 이제 사용을 원하는 부분에 변수명을 컴포넌트로 선언해준다.
<Box></Box>- Props를 사용하여 비슷한 컴포넌트의 색깔 스타일만 바꿀수도있다.
import styled from 'styled-components'; let YellowBtn = styled.button` background : ${ props => props.bg }; // 이부분 color : black; padding : 10px; `; function Detail(){ return ( <div> <YellowBtn bg="orange">오렌지색 버튼임</YellowBtn> <YellowBtn bg="blue">파란색 버튼임</YellowBtn> </div> ) }Q. 저거 ${ } 이거 무슨 문법임?
A. 자바스크립트 `` 백틱 따옴표 안에 적어도 문자를 만들 수 있는데
백틱으로 만든 문자 중간에 변수같은걸 넣고 싶을 때 ${ 변수명 } 이렇게 쓸 수 있습니다.장점
- CSS 파일 오픈할 필요없이 JS 파일에서 바로 스타일넣을 수 있습니다.
- 여기 적은 스타일이 다른 JS 파일로 오염되지 않습니다.
- 페이지 로딩시간 단축됩니다.
단점
- JS 파일이 매우 복잡해집니다.
- 컴포넌트가 styled 인지 아니면 일반 컴포넌트인지 구분도 어렵습니다.
- CSS 담당하는 디자이너가 있다면 협업시 styled-components 문법을 모른다면 그 사람이 CSS로 짠걸 styled-components 문법으로 다시 바꾸거나 그런 작업이 필요함
1.생성이 될수있다. (Mount)
2.재렌더링이 될수있다. (Updata)
3.삭제가 될수있다. (Unmount)
크게 3개의 생애주기 사이에서 hook 으로 코드실행을 개입시키는것을
Lifecylce hook이라고 한다 ex) A컴포넌트가 실행될때 Go 라는 hook코드를 실행
- react에선 이러한 Lifecyclehook을 사용하기위해
useEffect를 사용한다.- 사용법
import {useState, useEffect} from 'react'; function Detail(){ useEffect(()=>{ //여기적은 코드는 컴포넌트 로드 & 업데이트 마다 실행됨 console.log('안녕') }); let [count, setCount] = useState(0) return ( <button onClick={()=>{ setCount(count+1) }}>버튼</button> ) }- useEffect 안에 있는 consolelog 안녕은 랜더 될때마다 실행된다 때문에
button의 onClick 이벤트로 인한 count state 변화에 의해 안녕이 계속 출력됨- 근데
useEffect외부에 console.log를 하여도 똑같이 동작한다 다만 다른점은 내부에서 동작시 html이 먼저 동작하고 이후 내부코드가 돌아가기 때문에 시간절약이 가능하다 때문에오래걸리는 반복연산, 서버에서 데이터가져오는 작업, 타이머다는거 이런건 useEffect 안에 많이 적는다.
- 먼저 두가지 상태에 따라 바뀌는 UI 제작을 위해 State만들어주자.
- State 값이 true 일때만
<div>의 내용을 볼수있게 삼항연산자를 사용해주자.useEffect를 사용하여 내부에setTimout이라는 함수를 넣어주자
function Detail(){
let [alert, setAlert] = useState(true)
useEffect(()=>{
setTimeout(()=>{ setAlert(false) }, 2000)
}, [])
return (
{
alert == true
? <div className="alert alert-warning">
2초이내 구매시 할인
</div>
: null
} ) }
여기서 보면 잘 이해가지 않는 부분이있다. 바로 useEffect 의
[]부분
useEffect()의 둘째 파라미터로[ ]를 넣을 수 있는데 거기에 변수나state같은 것들을 넣을 수 있다.그렇게 하면[ ]에 있는 변수나state가 변할 때만useEffect안의 코드를 다음과 같이 실행한다.
useEffect(()=>{ 실행할코드 }, [count])
만약[]값에아무것도 안넣으면 컴포넌트 mount시 (로드시)
1회 실행하고 영영 실행해주지 않는다.useEffect(()=>{ 실행할코드 }, [])
useEffect 동작하기 전에 특정코드를 실행하고 싶으면
return ()=>{}안에 넣을 수 있다.clean up function이라고 부른다. 예를들어setTimeout()쓸 때마다 브라우저 안에 타이머가 하나 생깁니다.
근데 useEffect 안에 썼기 때문에 컴포넌트가 mount 될 때 마다 실행되고
잘못 코드를 짜면 타이머가 100개 1000개 생길 수도 있다.나중에 그런 버그를 방지하고 싶으면useEffect에서 타이머 만들기 전에 기존 타이머를 싹 제거하라고 코드를 짜면 되는데 그런거 짤 때 return ()=>{} 안에 짜면 된다.useEffect(()=>{ let a = setTimeout(()=>{ setAlert(false) }, 2000) return ()=>{ clearTimeout(a) } }, [])
useEffect를 사용해서 숫자만 입력가능한 input을 만들어보자
function Detail(){ let [num, setNum] = useState('') useEffect(()=>{ if (isNaN(num) == true){ alert('그러지마세요') } }, [num]) return ( <input onChange((e)=>{ setNum(e.target.value) }) /> ) }isNaN 은 String 이 들어가면 true 를 int 가들어가면 false를 뱉는 함수이다.
이를 통해 input 의 target.value에 문자가 들어가면 if 문이 동작한다.

GET,POST 요청을 그냥 보내게 되면 브라우저가 새로고침 된다 그렇기 때문에 우리는 Ajax 라는 브라우저 기능을 사용한다. Ajax를 사용하면 새로고침 없이 서버로 정보를 보내거나 가져오는 기능을 만들수있다. Ajax로 GET/POST를 요청하는 방법은 여러개가 있는데 그중 axios같은 외부 라이브러리를 이용하는 방법이 가장편하다.
npm install axios
- axios 사용할 위해 상단에서 import 해주자
axios.get(URL)이러한 방식으로 요청이 가능하다.- 데이터 가져온 결과는 then이후의 지정되는
result.data안에 들어있습니다.- 여러사항으로 실패했을 때 실행할 코드는
.catch()안에 적는다.
import axios from 'axios'
function App(){
return (
<button onClick={()=>{
axios.get('https://codingapple1.github.io/shop/data2.json')
.then((result)=>{
console.log(result.data)
})
.catch(()=>{
console.log('실패함')
})
}}>버튼</button>
)
}
응용 문제
버튼을 누르면 서버에서 상품데이터 3개를 가져와서 메인페이지에 상품카드 3개를 더 생성해보자.
- 서버에서 가져온 result.data를 확인하면 배열의 형태로 들어와있다.
- 이는 기존의 data.js에 있는 상품카드의 데이터 정보와 같은 형태이다.
- 그렇기때문에 기존의 상품카드 state에 새로운 result.data를 추가해준다.
//(생략) 바로 위의 코드와 같음 .then((result)=>{ let copy = [...art, ...result.data]; setArt(copy); console.log(art); }) //(생략) 바로 위의 코드와 같음만약 버튼을 2회 누를때 7,8,9번 상품도 가져오려면?
- 버튼의 입력횟수를 카운트 할 state를 하나 생성해야한다.
- 삼항연산자를 사용하여 버튼입력횟수 마다 각각 다른 데이터 정보를 가져온다.
- 세번이상 버튼을 누르면 알림 을 띄워 메시지를 보낸다.
btn < 1 ? axios.get('https://codingapple1.github.io/shop/data2.json') .then((result) => {setBtn(btn + 1); // (생략 위의 코드와 같음) : btn < 2 ? axios.get('https://codingapple1.github.io/shop/data3.json') .then((result) => {setBtn(btn + 1); // (생략 위의 코드와 같음) : alert('추가할 상품이없습니다).
1. post 요청방법?
axios.post('URL', {name : 'kim'})이거 실행하면 서버로 { name : 'kim' } 자료가 전송된다.
완료시 특정 코드를 실행하고 싶으면 이것도 역시 .then() 뒤에 붙이면 된다.2.동시에 AJAX 여러 요청
Promise.all( [axios.get('URL1'), axios.get('URL2')] )이러면 URL1, URL2로 GET요청을 동시에 해준다.
둘 다 완료시 특정 코드를 실행하고 싶으면 .then() 뒤에 붙이면 된다.3. 배열을 원래는 못가져온다?
object/array 이런거 못주고받는다.
근데 방금만해도 array 자료 받아온 것 같은데 그건 어떻게 한거냐면
object/array 자료에 따옴표를 쳐놓으면 된다.
"{"name" : "kim"}"이걸 JSON 이라고 한다.
JSON은 문자 취급을 받기 때문에 서버와 자유롭게 주고받을 수 있다.
그래서 실제로 결과.data 출력해보면 따옴표쳐진 JSON이 나와야하는데
axios 라이브러리는 JSON -> object/array 변환작업을 자동으로 해줘서
출력해보면 object/array가 나온다.4. 자주하는 실수
- ajax요청으로 데이터를 가져와서
- state에 저장하라고 코드를 짜놨고
- state를 html에 넣어서 보여달라고
<div> {state.어쩌구} </div>
이렇게 코드 짰는데.잘 될 것 같은데 이 상황에서 state가 텅 비어있다고 에러가 나는 경우가 많다.
이유는 ajax 요청보다 html 렌더링이 더 빨라서 그럴 수 있다.
state안에 뭐가 들어있으면 보여달라고 if문 같은걸 추가하거나 그러면 된다.
간단한 작업이라도 3Step 을 통해 천천히 진행하자
1. html css로 디자인 미리 완성해놓고
2. UI의 현재 상태를 저장할 state 하나 만들고
3. state에 따라서 UI가 어떻게 보일지 작성하면 된다고 했다.
1)디자인은 react-bootStrap 의 nav 를 검색하여 참고했다
<Nav variant="tabs" defaultActiveKey="link0">
<Nav.Item> //defaultActiveKey 여기는 페이지 로드시 어떤 버튼에 눌린효과를 줄지 결정하는 부분입니다.
<Nav.Link eventKey="link0">버튼0</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="link1">버튼1</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link eventKey="link2">버튼2</Nav.Link>
</Nav.Item>
</Nav>
<div>내용0</div>
<div>내용1</div>
<div>내용2</div>
2) UI의 현재 상태를 저장할 state 하나 만들기
0,1,2 3가지의 상태가 필요하기 때문에 초기값은 0으로 선언
function Detail(){
let [탭, 탭변경] = useState(0)
(생략)
}
3) state에 따라서 UI가 어떻게 보일지 작성
삼항연산자,컴포넌트,배열 어떤 방식으로 하든지 자유다. 여러 방식대로 한번 짜보자
3-1) function외부에 컴포넌트로 만들기
props.tab 으로 상속 받지않아도{탭} 으로 바로 state를 가져올수있다.
그리고 외부에 컴포넌트로 사용하면return을 사용해야한다.
function Detail(){
let [탭, 탭변경] = useState(0)
return (
<TabContent/>
)
}
function TabContent({탭}){
if (탭 === 0){
return <div>내용0</div>
}
if (탭 === 1){
return <div>내용1</div>
}
if (탭 === 2){
return <div>내용2</div>
}
}
3-2) array 를 사용해서 if 문없이 만들어보자
아래와 같은 형식은 [A배열] [B배열]=상수 형태임으로 상수 배열 B를 통해
A배열의 위치를 특정해 가져올수있다.
function TabContent(props){
return [ <div>내용0</div>, <div>내용1</div>, <div>내용2</div> ]
[props.탭]
}
애니메이션을 만들기위한 step
애니메이션 만들고 싶으면
1. 애니메이션 동작 전 스타일을 담을 className 만들기
2. 애니메이션 동작 후 스타일을 담을 className 만들기
3. transition 속성도 추가
4. 원할 때 2번 탈부착
탭의 내용이 서서히 등장하는 fade in 애니메이션을 만들어보자
1) 애니메이션 동작 전 2. 애니메이션 동작 후 className 만들기
.start {
opacity : 0
}
.end {
opacity : 1;
}
3) transition 추가
transition은 "해당 속성이 변할 때 서서히 변경해주셈~" 이라는 뜻이다. 그럼 이제 원하는 <div> 요소에 start 넣어두고 end 를 탈부착할 때 마다 fade in이 된다.
.start {
opacity : 0
}
.end {
opacity : 1;
transition : opacity 0.5s;
}
4) 원할 때 end 부착
동작원리를 알아보면 start가 실행되고 end가 나중에 뒤에 붙어 순간적으로
className='start end'가 되었을때 opacity 투명도가 fade in되는 것이다.
이제 "버튼을 누를 때 className에 end를 부착해주세요" 라고 코드짜면 애니메이션 동작한다.useEffect 를통해 활용할수 있을거 같다.
useEffect 쓰면 특정 state 아니면 props가 변할 때 마다 코드실행이 가능하다,
그래서 "탭이라는 state가 변할 때 end를 저기 부착해주세요" 라고 코드를 짜면 좋을듯
function TabContent({탭}){
let [fade, setFade] = useState('')
useEffect(()=>{
setFade('end')
}, [탭])
return (
<div className={'start ' + fade}>
{ [<div>내용0</div>, <div>내용1</div>, <div>내용2</div>][탭] }
</div>
)
}
5) 첫번째 동작에러
위의 코드가 동작하지 않는 이유는 start -> start end의 과정처럼 떼었다 붙여져야 하는데 바로 붙은 상태가 되버리기 때문이다. 이를 위해 우리는 setFade('') 를 작성
해주어야한다 이때 이전에 배운 cleanUp코드를 활용하면 아주좋다.
return () => {
setFade('');
6) 두번째 동작에러
clean up 으로 end 를 떼어줘도 동작하지 않는다? 이는 react 의
automatic batch 라는 기능때문인데 쉽게 말해 state의 변경 함수가 연달아 여러개 처리 될때 부하를 막기위해 계속 렌더링 하는것이 아닌 모두 처리된 이후의 마지막 state 상태만 렌더링 하는 기능이다. 이때문에 우리는 state에 약간의 딜레이를 부여해 렌더링이 겹치는걸 방지해야 한다. 저번에 사용했던 TimeOut 함수를 써보자.
function TabContent({탭}){
let [fade, setFade] = useState('')
useEffect(()=>{
let a = setTImeout(()=>{ setFade('end') }, 100)
return ()=>{
setFade('')
clearTimeout(a);
}
}, [탭])
return (
<div className={'start ' + fade}>
{ [<div>내용0</div>, <div>내용1</div>, <div>내용2</div>][탭] }
</div>
)
}
- 새로운 페이지를 만들기위해 먼저
Route와nav에 추가해주자.
<Route path="/cart" element={ <Cart/> } />- Route에 담기에 페이지가 크기 때문에 컴포넌트로 분류하고 import 해주자.
- 편하게 제작하기위해
bootStrap에서Table에 대한 양식을 가져와주자.<thead>태그로 가로를th태그로 컬럼을 나누고td태그로 속성을 구분<Table> <thead> <tr> <th>#</th> <th>상품명</th> <th>수량</th> <th>변경하기</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>안녕</td> <td>안녕</td> <td>안녕</td> </tr> </tbody> </Table>- 장바구니 기능을 사용하려면 상단의 App.js에서 State를 가져와야하는데 이를 편하게 하기위해 Redux를 배워볼것이다.
Redux란?
Redux는 props 없이 state를 공유할 수 있게 도와주는 라이브러리이다.
js 파일 하나에 여러 state들을 보관할수있어 모든 컴포넌트가 직접 꺼내쓸수있다.
npm install @reduxjs/toolkit react-reduxredux 를 설치해주자.
근데 설치하기 전에 package.json 파일을 열어react``react-dom항목의 버전을 확인하자. 두개가 18.1.x 이상이면 사용가능하다,.Redux 세팅
import { configureStore } from '@reduxjs/toolkit' export default configureStore({ reducer: { } })아무데나 store.js 파일을 만들어서 위 코드를 넣어주자 아까 말했던 state들을 보관하는 파일이다..
import { Provider } from "react-redux"; import store from './store.js' const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider> </React.StrictMode> );
index.js파일가서Provider라는 컴포넌트와 아까 작성한 파일을 import 해옵니다. 그리고 밑에 이걸로 을 감싸면 된다.이제 과 그 모든 자식컴포넌트들은
store.js에 있던state를 맘대로 꺼내쓸 수 있다.
Redux store에 State를 보관해보자.
- store.js 를열어 createSlice()를 먼저 import 해준다.
- createSlice 에
{ name : 'state이름', initialState : 'state값' }- 2번과 같이 선언해주면 state 하나가 생성가능하게된다.
- configureStore() 라는 변수도 import 해준다.
- configureStore() 안에
{작명 : createSlice만든거.reducer}선언
import { configureStore, createSlice } from '@reduxjs/toolkit'
let user = createSlice({
name : 'user',
initialState : 'kim'
})
export default configureStore({
reducer: {
user : user.reducer
}
})
Redux store에 있던 state를 가져다 써보자
아무 컴포넌트에서
useSelector((state) => { return state } )쓰면 store에 있던 모든 state가 그 자리에 남는다.
(Cart.js)
import { useSelector } from "react-redux"
function Cart(){
let a = useSelector((state) => { return state } )
console.log(a)
return (생략)
}
응용문제
아래와 같은 오브젝트형 데이터를 Store에 담으려면 똑같이
name을 설정하고
initialState선언에 오브젝트 그대로 넣어주면 state로 선언이 가능하고
컴포넌트에서 사용시{a.name[0]?.id}이러한 형식으로 배열을 불러서 사용[ {id : 0, name : 'White and Black', count : 2}, {id : 2, name : 'Grey Yordan', count : 1} ]
1.store.js에 state변경해주는 함수를 만들고
2.export 한다.
3. 필요할때 import 해서 사용하면 되는데 dispatch()로 함수를 감싸서 사용해야한다.
1. store.js 안에 state를 수정해주는 함수를 생성한다.
let user = createSlice({ name : 'user', initialState : 'kim', reducers : { changeName(state){ return 'john ' + state } } })
slice안에reducers :{}를 열고 그안에 함수를 생성하면 된다.
함수 작명은 원하는대로 가능하며 파라미터 하나를 작명해 기존state를 사용할수있다 return 우측에 새로운state를 입력하면
그 값으로 기존state를 갈아치워준다.
2. 다른곳에서 쓰기좋게 export 해준다.
export let { changeName } = user.actions
이런 코드를 store.js 밑에 추가하면 된다.slice이름.actions
라고 적으면 state 변경함수가 전부 그자리에 출력된다.
그걸 변수에 저장했다가 export 하라는 코드이다.
3. import 해서 사용한다. 근데 dispatch()로 감싸서
(Cart.js) import { useDispatch, useSelector } from "react-redux" import { changeName } from "./../store.js" (생략) <button onClick={()=>{ dispatch(changeName()) }}>버튼임</button>
Cart.js에서 버튼을 만들어ChangeName 함수를 실행하는 코드이다.
store.js에서 원하는state변경함수를 가져오고
useDispatch라는 것도 라이브러리에서 가져온다 그리고dispatch(state변경함수())이런식으로 감싸서 실행하면 state가 변경된다.
왜이런식으로 할까??
컴포넌트 100개에서 직접 'kim' 이라는 state 변경하다가 갑자기 'kim'이 123이 되어버리는 버그가 발생할수있다.범인 찾으려고 컴포넌트 100개를 다 뒤져야하는데 만약 state 수정함수를 store.js에 미리 만들어두고
컴포넌트는 그거 실행해달라고 부탁만 하는 식으로 코드를 짜놓으면 'kim'이 123이 되어버리는 버그가 발생했을 때 범인찾기가 수월할것이다.
범인은 무조건 store.js에 있으니까. (수정함수를 잘 만들어놨다면)
redux state가 array/object 인 경우 변경하려면?
{name:'kim',age:20}이렇게 생긴 자료를 state 로 만들어보자kim이라는 내용을park으로 변경하는 함수를 만들고싶으면 state변경함수는 아래처럼 만든다.이러한 코드를 사용하면let user = createSlice({ name : 'user', initialState : {name : 'kim', age : 20}, reducers : { changeName(state){ return {name : 'park', age : 20} } } })changeName()함수 사용시 당연히변경된다
그러면 아래처럼 state를 직접 수정하게 하면 어떻게 될까이러한 코드를 사용해도let user = createSlce({ (생략) reducers : { changeName(state){ state.name = 'park' } } })immer.js라이브러리 를 통해 잘 동작한다.
결론은array/object자료의 경우 state변경은 직접 수정해도 되니까
직접 수정하자 때문에 state를 만들때 문자나 숫자하나만 필요해도 redux에선 일부러 object 아니면 array에 담는 경우가 있는데 이는 수정이 편리해서이다.
응용 문제 1
사이트에 버튼을 만들어 클릭시 age 항목이 +1 되게 만들어보자
let user = createSlice({ name : 'user', initialState : {name : 'kim', age : 20}, reducers : { increase(state){ state.age += 1 } } })이렇게 increase라는 함수 만들어 export 하고
필요한 곳에서 import 해서 dispatch(increase()) 하면 +1 된다.
응용문제2
그러면 변경함수가 여러개 필요하면 여러 함수를 전부 redux에 넣음??
동작은 같으나 횟수나 인자가 다를경우 우리는파라미터 문법을 사용할수있다.
아래처럼 코드를 사용하면 action이라는 파라미터를 주어 함수를 사용하는 컴포넌트에서 값을 설정해 원하는 만큼 함수의 실행을 도울수있다.
이때 꼭 파라미터 뒤에는.payload를 사용해주자let user = createSlice({ name : 'user', initialState : {name : 'kim', age : 20}, reducers : { increase(state, a){ state.age += a.payload } } })파라미터를 사용해주면
increase(10)하면 10을 더하고
increase(100하면 100을 더할것이다.
응용문제1
장바구니 오브젝트에서 수량+1 기능을 만들어보자
let cart = createSlice({ name : 'cart', initialState : [ {id : 0, name : 'White and Black', count : 2}, {id : 2, name : 'Grey Yordan', count : 1} ], reducers : { addCount(state, action){ state[action.payload].count++ } } })
addCount라는 함수를 만들어 파라미터로action을 주어addCount(0)하면 0번째 상품이 +1addCount(1)하면 1번째 상품이 +1 된다. export해서
필요할때 사용하면 될거같고 장바구니 컴포넌트 에선 대충 이렇게 쓰면 될거같다.<button onClick={()=>{ dispatch(addCount(i)) }}>+</button>어짜피 map을 통해 반복 되니까 파라미터 i를 써서 열마다 새로운 i값을 받을수있다. 0번째 버튼을 누르면
addCount(0)실행 이런식으로
응용문제 1(심화)
그런데 만약 테이블의 정렬 순서가 바뀌면??
저러한 방식으로 코드를 사용하면 오브젝트안의 데이터 순서가 바뀔경우
버그가 발생할것이다. 그렇기때문에 우리는 데이터마다 가진
고유의 id 값을 확인하여 count++ 를 해줄것이다.
먼저 장바구니 컴포넌트의 코드를 조금 수정해주자.<button onClick={()=>{ dispatch(addCount(state.cart[i].id)) }}> +</button>이런식으로 코드를 짜면 dispatch안의 addCount 내용은
state.cart[i]에 대한 id 값즉 사용자가 선택한 테이블의 id 값을 가지고있을것이고 그 값은 action.payload 로 전송됨.이제 addCount 함수를 수정해보자
"payload와 같은 id를 가진 상품을 찾아서 +1 해달라~"이 말을
js로 풀어서 써넣어야한다.state 오브젝트안에서payload와 같은id를 찾아서 그 값을 가진 테이블의count 1증가해달라 해야한다 그럼 코드를 보자let cart = createSlice({ name : 'cart', initialState : [ {id : 0, name : 'White and Black', count : 2}, {id : 2, name : 'Grey Yordan', count : 1} ], reducers : { addCount(state, action){ let 번호 = state.findIndex((a)=>{ return a.id === action.payload }) state[번호].count++ } } })array 자료에서 원하는 항목을 찾으려면 반복문, find(), findIndex() 사용!!!!
findIndex()는 array 뒤에 붙일 수 있다.- 안에 콜백함수넣고 return 뒤에 조건식도 넣으면 된다.
a라는 파라미터는 array 안에 있던 하나하나의 자료다.- array에 있던 자료를 다 꺼내서 조건식에 대입해보는데
조건식이 참이면 그게 몇번째 자료인지 알려준다.그래서 위의 코드는
a.id와payload가 같으면 그게 몇번째 자료인지
변수에 저장하라는 소리다.
응용문제 2
주문버튼누르면 state에 새로운 상품추가
상세페이지의 주문하기 버튼을 누르면 장바구니 state에 항목이
하나 추가되는 기능을 만들어보자 이것도 state 변경함수 만들고
export하고 import해서 사용한다.let cart = createSlice({ name : 'cart', initialState : [ {id : 0, name : 'White and Black', count : 2}, {id : 2, name : 'Grey Yordan', count : 1} ], reducers : { addCount() (생략) addItem(state, action){ state.push(action.payload) } } })
addItem()이라는 함수를 만들어 state 오브젝트에 새로운 배열을
action.payload에 가져와push하는 과정을 진행해보자.(Detail.js)
<div className="col-md-6"> <h4 className="pt-5">{찾은상품.title}</h4> <p>{찾은상품.content}</p> <p>{찾은상품.price}원</p> <button className="btn btn-danger" onClick={()=>{ dispatch(addItem( {id : 1, name : 'Red Knit', count : 1} )) }}>주문하기</button> </div> </div>상세페이지(Detail.js)에서도
useDispatch를 import 해주고
만든addItem()함수도 import 해주자. 그리고 위와같이
id,name,count등을 넣어주면 새로운 데이터가 배열안에 들어간다.
우리는 찾은 상품의id,title등을 알고있으니 저런식으로 하드코딩이아닌addItem({id: finded.id, name: finded.title, count: 1,})이와 같은 방법으로
state를 연결해주면 더 좋을거같다.
응용문제 3
표의 행마다 삭제버튼 만들고 그거 누르면 상품이 삭제되게 만들려면?
문제를 해결하려면 일단 배열에서 특정 값을 제거하는 함수를 알아야한다
검색으로pop과splice라는 함수를 알게되었다 이중 응용1번 처럼
장바구니의 값을 정렬할때 위치가 바뀔수도 있으니 고유 번호인id값을
기준으로 진행하게 할려면 아래와 같이 코드를 사용할수 있을것이다.DeleteItem(state, action) { let paynum = state.findIndex((a) => { return a.id === action.payload; }); state.splice(paynum, 1); },
state.spicle(paynum,1)은 이제 배열의 paynum에 해당하는
객체부터 ~1만큼 제거하겠다는 뜻이다 즉 paynum에 해당하는 객체만 삭제<button onClick={() => {dispatch(DeleteItem(state.cartdata[i].id));}}사용하는
cart.js부분에도 응용 1과 같이cartdata.id값을 보내주면 끝.
응용문제 4
주문하기 버튼 누를 때 이미 상품이 state안에 있으면 추가가 아니라 기존 항목 수량증가만?
기존의
addItem에서 if 문을 사용해 장바구니에 이미 존재하는 id 는
count++하고 없는 id 만 add 해주는 과정이 필요할꺼같다.addItem(state, action) { // id 를 확인해 이미 장바구니에 있는 id면 count+ 아니면 새로 push let paynum = state.findIndex((a) => a.id === action.payload.id); if (paynum >= 0) { state[paynum].count++; console.log(paynum); } else { state.push(action.payload); console.log(paynum); } },여기서 중요하게 봐야할 부분은
paynum >=0부분이다paynum에
어떤 내용이 뜨는지 정확히 보기위해console.log로 확인해보니
장바구니에 있는 값이면 그 id값의 배열 번호가 뜨고 만약 없으면
-1음수가 나왔다 그러므로 if 문을 사용하여 0보다 크면 count++
작으면 새로 push하는 코드를 작성했다.onClick={() => { dispatch( addItem({ id: finded.id, name: finded.title, count: 1, }) );
새로고침하면 모든 state 데이터는 리셋된다. 왜냐면 새로고침하면
브라우저는 html css js 파일들을 첨부터 다시 읽기 때문이다.
이게 싫다면 state 데이터를 서버로 보내서 DB에 저장하거나 하면 된다.
내가 서버나 DB 지식이 없다면 localStorage를 이용해도 된다.
유저의 브라우저에 몰래 정보를 저장하고 싶을 때 쓰는 공간이다.
크롬개발자 도구에서 Application 탭 들어가면 볼수있으며 사이트마다 5MB정도의 문자 데이터를 저장할 수 있다.
object 자료랑 비슷하게 key/value 형태로 저장하며
유저가 브라우저 청소를 하지 않는 이상 영구적으로 남아있는다.
밑에 있는 Session Storage도 똑같은데 브라우저 끄면 삭제되는 휘발성.사용법
js 파일 아무데서나 다음 문법을 쓰면 localStorage에
데이터 입출력 할 수 있다. 차례로 추가, 읽기, 삭제 문법이다.localStorage.setItem('데이터이름', '데이터'); localStorage.getItem('데이터이름'); localStorage.removeItem('데이터이름')object 는 못함?
localStorage에 array/object자료를 저장하려면 문자만 저장할 수 있는 공간이라array/object를 바로 저장할 수는 없다. 강제로 저장
해보면 문자로 바꿔서 저장해주는데 그럼array/object자료가 깨진다.
그래서 편법이 하나 있는데array/object -> JSON이렇게 변환해서
저장하면 된다.JSON은 문자취급을 받아서 그렇다. JSON은 그냥 따옴표친 array/object 자료다.localStorage.setItem('obj', JSON.stringify({name:'kim'}) );이런식으로 코드를 만들면
obj라는 key를 가지고name:kim이라는
object 값의value를 가진 값을 저장할수있다.var a = localStorage.getItem('obj'); var b = JSON.parse(a)당연히 꺼낼때도 그냥 꺼내면 JSON값을 그대로 가져오기 때문에
JSON.parse를 사용해서 array/object값으로 다시 변경해줘야한다.응용문제 1
localStorage 를 사용해서 main페이지에서 최근 열람한 상품들을 간단하게
보여주는 ui를 만들어보자 그러기 위해선 id 값들을 가져와야한다.
1.먼저 최상단의 App.js 에서 시작시 localStorage에 배열을 넣어 초기화하자.useEffect(()=>{ localStorage.setItem('watched', JSON.stringify( [] )) },[])2.
Detail.js에서 get 을 통해watched배열을 가져와보자.
3. 가져온 배열에서 현재있는 상품 페이지의 id를 넣는다.
4. set으로 다시 배열을 localstorage 에 저장하자.(Detail.js) useEffect(()=>{ let 꺼낸거 = localStorage.getItem('watched') 꺼낸거 = JSON.parse(꺼낸거) 꺼낸거.push(찾은상품.id) localStorage.setItem('watched', JSON.stringify(꺼낸거)) }, [])
- 작동은 잘하지만 이미본 상품도 계속 추가되는 버그가 있다 고쳐보자
- 상품 id에 이미 같은 값이 있으면 추가하지말라고 if문을 사용할수있지만
set이란 문법을 통해 중복을 제거하고 다시array로 감싸는 방법을 사용해보자 아래의 두 문장을 중간에 넣어주면 잘 동작 할거다.7.근데 사이트 새로고침시 배열이 전부 사라지게 되는데 이는(생략) 꺼낸거 = new Set(꺼낸거) 꺼낸거 = Array.from(꺼낸거) (생략)
우리가 처음에 선언한app.js의watched에 대한useEffect
때문이다.if문을 통해 배열이null값이 아니라면 실행하지 않겠다고 선언하자.
ajax 요청하다보면 아래와 같은 기능들이 필요해진다.
- 몇초마다 자동으로 데이터 다시 가져오게 하려면?
- 요청실패시 몇초 간격으로 재시도?
- 다음 페이지 미리가져오기?
- ajax 성공/실패시 각각 다른 html을 보여주려면?
이러한 기능들을 react-quert를 사용하면 쉽게 넣을수있다.
react-query 세팅
- 설치:
npm install @tanstack/react-query- 세팅: index.js 로 가서 아래의 코드 Import
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'- 원래의
index.js코드에 2번을 import 하고QueryClient태그로 감싸준다.<QueryClientProvider client={queryClient}> <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider> </QueryClientProvider>- https://codingapple.com/course-status/ 기능
크롬 확장프로그램 : React Developer Tools
props를 보냈는데 출력이 안된다거나 이미지를 넣었는데 안보이는
버그같은게 생기면 개발자도구를 켜서Elements 탭살펴보면 되는데
여기선 짠 코드가 실제html css로 변환되어서 보인다.
그게 싫고 컴포넌트로 미리보고 싶으면 리액트 개발자도구를 설치해서 켜보면 됨.
위의 확장 프로그램을 사용하여 컴포넌트를 미리 확인 할수있으며
왼쪽상단 아이콘눌러서 컴포넌트 찍어보면 거기 있는
state, props 들을 조회가 가능하다.
Profiler 탭에서는 녹화버튼을 누르고 앱에서의 동작 버튼클릭 페이지이동
들을 해본후 녹화를 종료하면 방금 렌더링된 모든 컴포넌트의 렌더링 시간을
측정해준다 이를통해 딜레이 걸리는 문제들을 확인이 가능하다.
Redux전용developer tools도 있으니 나중에 사용해보자.
lazy import
react 를 다짜고 나서 배포를 할려고
build하는 순간 우리가 짠
모든 코드의 내용이 하나의js에 묶여서 배포된다. 이렇게되면
js의 용량이 크기때문에 사용자가 처음 접속시 시간이 오래걸리것이며
이를 자원을 크게 잡아 먹는다고 말할수있다. 그렇기 때문에 우리는
lazy import라는 것을 사용해 메인에서 바로 처리하지 않는 컴포넌트를 사용하기 전까지 자원을 받지 않게 할것이다.
예를들어 아래의 코드는 App.js의 시작시 사용되지않음으로(App.js) import Detail from './routes/Detail.js' import Cart from './routes/Cart.js'다음과 같이 바꿔줄것이다.
(App.js) import {lazy, Suspense, useEffect, useState} from 'react' const Detail = lazy( () => import('./routes/Detail.js') ) const Cart = lazy( () => import('./routes/Cart.js') )물론
lazy import도 사용이 가능한데 이는 선언한 컴포넌트들이
필요하면 그때 load 해주세요 라는 의미를 담고있다.
lazy를 사용하면 당연히 Detail 컴포넌트 로드까지 지연시간이 발생한다.
1. Suspense 라는거 import 해오고
2. Detail 컴포넌트를 감싸면 Detail 컴포넌트가 로딩중일 때 대신 보여줄 html 작성도 가능하다. 귀찮으면<Suspense>이걸로<Routes>전부 감싸도 된다.<Suspense fallback={ <div>로딩중임</div> }> <Detail shoes={shoes} /> </Suspense>
##21. 성능 개선2 재렌더링 막는 memo, useMemo
컴포넌트가 재렌더링되면 거기 안에 있는 자식컴포넌트는 항상 함께 재렌더링된다.리액트는 그렇게 대충 무식하게 동작하는데 평소엔 별 문제가 없겠지만 자식컴포넌트가 렌더링시간이 1초나 걸리는 무거운 컴포넌트면 곤란해진다.
부모컴포넌트에 있는 버튼 누를 때 마다 1초 버벅이는 불상사가 발생하기때문
그럴 땐 자식을memo로 감싸놓으면 된다.
function Child(){
console.log('재렌더링됨')
return <div>자식임</div>
}
function Cart(){
let [count, setCount] = useState(0)
return (
<Child />
<button onClick={()=>{ setCount(count+1) }}> + </button>
)
}
테스트용 자식 컴포넌트 를 하나 만들어보았다. 이제 만약
cart컴포넌트가
재렌더링 될때child컴포넌트 또한 재렌더링 될것이다. 만약child가 이처럼 가벼우면 상관없는데 1초 정도 걸리는 동작이 들어가면 버튼 하나
누를때 마다 동작이 더뎌질것이기 떄문에 memo를 통해 꼭 필요할때만
재렌더링 되게 바꿔줄수있다.
위의 코드에서 child 함수부분만 살짝바꿔줘 보자
let Child = memo( function(){
console.log('재렌더링됨')
return <div>자식임</div>
})
이제 cart가 재렌더링 될때 같이 재렌더링 되는게아닌 child자체의
state가 변경 될때만console이 찍힐것이다. 즉button을
누를 때만 작동된다는 뜻이다.import 도 까먹지 말고 해주자
import {memo, useState} from 'react'
그렇다고 memo를 전부 사용하면 문제가 발생되는데 이는 memo의 동작
안에 기존의 state와 이후 state의 변경점을 확인하는 연산이 있기 때문에
많은 사용은 오히려 부하를 주기 때문이다.
##22. 성능 개선 3 useTransition
렌더링시간이 매우 오래걸리는 컴포넌트가 있다고 했을때
버튼클릭, 타이핑할 때 마다 그 컴포넌트를 렌더링해야한다면
이상하게 버튼클릭, 타이핑 반응속도도 느려진다 개선방법을 알아보자.
당연히 그 컴포넌트 안의 html 갯수를 줄이면 대부분 해결되는데
근데 그런게 안되면 useTransition 기능을 쓰면 된다.
import {useState} from 'react'
let a = new Array(10000).fill(0)
function App(){
let [name, setName] = useState('')
return (
<div>
<input onChange={ (e)=>{ setName(e.target.value) }}/>
{
a.map(()=>{
return <div>{name}</div>
})
}
</div>
)
}
위처럼 사용자가 input에 입력한 정보를
map을 통한 10000개의
값을 가진 배열에 집어넣어<div>안에 보여주는 무식한 코드를
만들어보자 유저가<input>에 타이핑하면 그 글자를<div>
1만개안에 집어넣어줘야하는데<div>1만개 렌더링해주느라
<input>도 많은 지연시간이 발생한다.
import {useState, useTransition} from 'react'
let a = new Array(10000).fill(0)
function App(){
let [name, setName] = useState('')
let [isPending, startTransition] = useTransition()
return (
<div>
<input onChange={ (e)=>{
startTransition(()=>{
setName(e.target.value)
})
}}/>
{
a.map(()=>{
return <div>{name}</div>
})
}
</div>
)
useTransition()쓰면 그 자리에[변수, 함수]가 남는다.
우측에 있는startTransition()함수로 state변경함수 같은걸
묶으면 그걸 다른 코드들보다 나중에 처리해준다.
그래서<input>타이핑같이 즉각 반응해야하는걸 우선 처리해줄 수 있다.
물론 근본적인 성능개선이라기보단 특정코드의 실행시점을 뒤로 옮겨주는 것일 뿐이다.
isPending은 어디다 쓰냐면
{ isPending ? "로딩중기다리셈" : a.map(()=>{ return <div>{name}</div> }) }
startTransition()으로 감싼 코드가 처리중일 때true로 변하는 변수다.그래서 이런 식으로 코드짜는 것도 가능 위의 코드는useTransition으로 감싼게 처리완료되면<div>{name}</div>이게 보이겠군요.