컴포넌트 파일 분리, 폴더 구조 변경
상세 페이지 컴포넌트를 생성해보려하는데 App.tsx 가 파일이 너무 길어진다. 따라서 컴포넌트를 별도의 파일로 분리해보자
// Detail.tsx
import React, { useState, FC } from 'react';
import {Button, Navbar, Container, Nav, Col, Row} from 'react-bootstrap'
const Detail : React.FC = () => {
return(
<Container>
<Row>
<Col>
<img src = {process.env.PUBLIC_URL + '/coffee' + 1 + '.jpg'} width = '80%'></img>
</Col>
<Col>
<h5>커피이름</h5>
<h6>커피상세설명</h6>
<p>커피가격</p>
<button className='btn btn-primary'>캐러어에 담기</button>
</Col>
</Row>
</Container>
)
}
export default Detail;
똑같이 컴포넌트를 만든다. 단, export 를 해주어야 다른 파일에서 사용할 수 있다.
Import 해주기 전에 한가지 더 해야할 일이 있다. 지금은 모든 소스파일이 src 폴더에 다 들어있지만, 용도에 따라 패키지 구조를 만들어 줄 필요가 있다.
일단은 data 와 pages 폴더를 만들어 관리하기로 한다.
마지막으로 App.tsx에서 import 를 해주면 끝이다.
import Detail from './pages/Detail';
//...
<Route path='/detail' element = {<Detail></Detail>}></Route>
useNavigate
라우터 라이브러리에서 제공하는 또 다른 방식이 있는데 useNavigate 라는 것이다.
앞에서 배웠던 Link 와 페이지를 이동한다는 공통점이 있지만 차이점도 있다.
Link는 클릭시 바로 이동하는 로직에 필요하지만, useNavigate는 함수호출 방식이기 때문에 특정 조건을 충족할 경우에 페이지 이동을 하게 하는 등 페이지 전환시 추가로 처리해야할 로직이 있을 경우 사용한다.
let navigate = useNavigate();
<Nav.Link onClick={()=>{navigate('/')}}>Home</Nav.Link>
404 처리
정해지지 않은 경로로 요청이 들어오면 사용자에게 이를 알려주어야한다. 이때 조건문에서 else 처럼 지정된 경로 외의 나머지를 한번에 묶어줄 수 있는 방식이 있다.
<Route path='*' element = {"존재하지 않는 요청입니다. 다시 확인해주세요"}></Route>
* 기호를 사용하면 예외적인 요청들을 한번에 처리해 줄 수 있다.
nested routes
회사 정보를 만들어준다.
//About.tsx
const About : FC = () =>{
return(
<div>
<h1>회사정보</h1>
<img src = {process.env.PUBLIC_URL + "../lab.jpg"} width = "70%"></img>
</div>
)
}
회사 정보는 /about 경로에 들어가면 볼 수 있다.
//App.tsx
<Route path='/about' element = {<About></About>}>
<Route path='member' element = {<div>대표자</div>}></Route>
<Route path='location' element = {<div>서울특별시</div>}></Route>
</Route>
라우터 설정도 해준다. 이때 about 하위로 member와 location 주소를 추가해 주고자 한다. about 라우트의 하위에 라우터를 중첩해서 추가로 넣어주면 제대로 작동할까?
답은 작동하지 않는다. 이런 중첩된 라우터에 필요한 것이 Outlet 이다.
//About.tsx
const About : FC = () =>{
return(
<div>
<h1>회사정보</h1>
<img src = {process.env.PUBLIC_URL + "../lab.jpg"} width = "70%"></img>
<Outlet></Outlet>
</div>
)
}
컴포넌트에 Outlet 컴포넌트를 넣어주면 제대로 동작하는 것을 알 수 있다. Outlet은 부모 라우트의 컴포넌트에서 자식 라우트 컴포넌트의 위치를 지정해주는 기능이라고 할 수 있다.
interface import
인터페이스를 생성과 동시에 export 해줄 수 있다.
export interface Coffee{
id : number;
title : string,
content : string;
price : number;
}
이 인터페이스를 가져다 사용하려면 다른 파일에서 import 해주면 된다.
import {Coffee} from '../App';
url 파라미터
상세페이지를 표현하기 위해서는 동적으로 페이지를 생성하기 위해 detail 뒤에 id 값 같은 것을 붙여주어야 한다. 라우터에서 url 파라미터 사용법은 다음과 같다.
<Route path='/detail/:id' element = {<Detail coffee = {coffee}></Detail>}></Route>
id 값이 url 파라미터 정보이다. 이를 컴포넌트에서 사용하려면 어떻게 해야할까?
useParams 라는 함수를 사용하면 된다.
//Detail.tsx
const {id} = useParams<{id? : string}>();
const parsedId : number | undefined = id? parseInt(id, 10) : 0;
타입 지정도 해줄 수 있다. 또한 파라미터로 들어오는 값은 항상 문자열이기 때문에 타입캐스팅을 해주어야 한다.
이제 이 값을 활용해서 상세 페이지를 동적으로 만들어 줄 수 있다.
Styled Components
html을 작성하고 css 파일에 스타일을 매번 적용하는 것은 번거로운 일이다. 따라서 아예 스타일링까지 된 컴포넌트를 생성해주는 라이브러리가 있는데 이것이 styled components 이다.
npm install styled-component
사용방법은 다음과 같다.
import styled from 'styled-components';
let BlueBtn = styled.button`
background : blue;
color: white;
padding : 20px;
`
<BlueBtn>팀버튼</BlueBtn>
백틱 기호 안에 스타일을 지정해주고 그 후로는 컴포넌트처럼 사용하면 된다.
그렇다면 이렇게 사용하는 것의 장점은 무엇일까? CSS 파일을 열 필요 없이 자바스크립트에서 바로 스타일을 적용할 수 있다. App.css 파일의 스타일은 모든 파일들에 공유가 되지만, style components는 다른 파일에 영향을 주지 않는다. 또 하나의 장점은 속도이다. 컴포넌트 안에 스타일이 내장되어있기 때문에 로딩 속도가 빠르다.
물론 단점도 있다. 가독성이 좀 떨어질 수도 있다. 또한 CSS 디자이너와의 협업에도 문제가 생길 수 있다.
props도 사용할 수 있다.
interface TeamBtnProps{
bg: string;
}
let ColoredBtn = styled.button<TeamBtnProps>`
background : ${(props) => props.bg};
color: white;
padding : 20px;
`
<ColoredBtn bg = 'blue'>팀버튼</ColoredBtn>
<ColoredBtn bg = 'red'>팀버튼</ColoredBtn>
<ColoredBtn bg = 'yellow'>팀버튼</ColoredBtn>
템플릿 리터럴을 사용해서 props를 사용할 수 있다.
컴포넌트 생명주기
컴포넌트의 생명주기에는 크게 3가지 단계가 있다.
- 페이지에 장착(mount)
- 업데이트(update)
- 필요 없으면 제거(unmount)
각각의 시점에 코드를 실행하게 요청 해주는데 이를 hook이라 한다.
useEffect
이전 버전의 리액트에서는 클래스 기반의 컴포넌트를 주로 사용하였다. 클래스 기반의 컴포넌트에서는 마치 생성자와 소멸자 처럼 훅이 명확하게 명시되어 있었다. 하지만 현재는 함수형 컴포넌트의 사용을 권장하고 있다. 이때는 useEffect를 사용한다.
useEffect(()=>{
console.log('라이프사이클 테스트');
})
useEffect 는 마운트 시점과 업데이트 될 때 동작한다.
그런데 한가지 이상한 점은 useEffect를 쓰지 않아도 코드는 컴포넌트 생성시점에 동작한다. 그렇다면 진짜 useEffect를 쓰는 이유는 무엇일까? useEffect안에 적힌 코드는 html 렌더링 이후에 동작한다. 일반적인 코드들은 순서대로 실행이 되지만 useEffect는 html 이후에 수행된다는 뜻이다. 이는 시간이 오래 걸리는 작업 등을 처리하는데 유용할 수 있다. useEffect를 사용하지 않으면 그런 작업이 있을 때 화면이 굉장히 늦게 뜰 것이다.
useEffect(()=>{
const fetchData = async () => {
for(let temp = 0; temp < 10000; temp++){
console.log('라이프사이클 테스트2');
}
}
fetchData();
})
useEffect에는 다른 매개변수를 넣을 수도 있다. 두번 째 매개변수로 상태나 변수를 줄 수 있는데 이 의미는 그 상태가 변경되었을 때만 콜백함수를 실행하라는 의미이다.
let [discount, setDiscount] = useState<boolean>(true);
useEffect(()=>{
const fetchData = async () => {
setTimeout(()=>{setDiscount(false)}, 3000)
}
fetchData();
}, [discount])
만약 빈 배열을 두번째 매개변수로 전달하면 이것의 의미는 딱 처음 마운트 되었을때만 한번 실행하고 그 이후로는 실행하지 않는다는 의미이다.
useEffect(()=>{
const fetchData = async () => {
setTimeout(()=>{setDiscount(false)}, 3000)
}
fetchData();
}, [])
처음처럼 아예 매개변수를 넣지 않으면 무슨 의미일까? 마운트 되었을 때도 작동하고, 업데이트 될때마다도 작동한다는 의미이다.
useEffect(()=>{
const fetchData = async () => {
setTimeout(()=>{setDiscount(false)}, 3000)
}
fetchData();
})
Clean up
useEffect 콜백함수 안에서 return 문에 콜백함수를 또 넣어줄 수 있다. 이러한 구조를 Clean up function 이라고 한다. 이것의 의미는 return 문의 함수를 먼저 실행하고 원래 콜백함수를 실행하라는 의미이다.
useEffect(()=>{
const fetchData = async () => {
setTimeout(()=>{setDiscount(false)}, 3000)
console.log(2);
}
fetchData();
return () =>{
console.log(1);
}
}, [])
콘솔창에는 1 2 순서로 찍히는 것을 알 수 있다.
실제 활용예시로 들어가보자. 현재는 페이지를 마운트할 때마다 타이머가 새로 생성된다. 이것이 누적 되면 오류가 발생할 여지가 있으므로 clean up 함수로 기존 타이머를 제거하는 코드를 넣어보자.
useEffect(()=>{
let timer : NodeJS.Timeout;
const fetchData = async () => {
timer= setTimeout(()=>{setDiscount(false)}, 3000)
console.log(2);
}
fetchData();
return () =>{
clearTimeout(timer);
console.log(1);
}
}, [])
이와 같이 코드를 만들면 타이머는 항상 한개만 존재하게 된다.
그 밖에는 어떤 쓸모가 있을까? 그 답은 서버요청의 사례에서 찾아볼 수 있다. 서버 요청을 할 때 기존의 요청은 제거하고 다시 새로운 요청을 하는 방식으로 활용할 수 있다.
Ajax
http의 메소드를 사용해서 서버에 요청을 하게되면 새로고침이 되어야 한다. 하지만 이 새로고침 없이 데이터를 주고 받을 수 있게 도와주는 기술이 있는데 이를 Ajax라고 한다. 이를 사용하는 방법은 크게 3가지가 있다.
- XMLHttpRequest 문법 사용
- fetch() 문법 사용
- axios 최신 외부 라이브러리 사용
axios가 가장 최신이고 편하므로 이 방법을 사용해보자.
Axios
npm으로 axios 라이브러리를 설치한다.
npm install axios
사용법은 다음과 같다.
import axios from 'axios';
<button onClick={()=>{
axios.get('https://jamsuham75.github.io/image/coffee.json')
.then((result)=>{
console.log(result.data)
})
.catch(()=>{
console.log('fail');
})
버튼을 클릭했을 때 axios를 통해 서버 주소에서 GET 해오는 코드이다. 가져온 데이터는 then 에서 받아서 처리해 줄 수 있다. 만약 오류가 날 경우 catch 문이 실행된다.
콘솔창에 가져온 데이터가 잘 표시되는 것을 알 수 있다.
window.location.reload()
화면을 새로고침 하는 함수이다. 그림 더보기의 토글 방식 적용에 사용되었다.
{
(expands === false)?
axios.get('https://jamsuham75.github.io/image/coffee.json')
.then((result)=>{
let coffeeMerge = [...coffee, ...result.data];
setCoffee(coffeeMerge);
setExpands(true);
setBtnIcon('🔺')
})
.catch(()=>{
console.log('fail');
}) :
window.location.reload();
setExpands(false);
setBtnIcon('🔻');
}
axios post
axios.post('/url', {coffee: '아메리카노})
현재는 서버가 없으므로 참고만 하자.
axios 여러개 동시에 처리
여러 요청이 모두 성공했을 때 처리하는 방법이 있다.
axios.get('url1');
axios.get('url2');
Promise.all([axios.get('url1'), axios.get('url2')])
.then(()=>{
})
axios 파싱
사실 서버에서 제공받는 json 배열등은 문자열로 전송되는 것이다. axios는 이 문자열을 파싱해서 우리가 필요로 하는 객체로 변환해준다. fetch의 경우는 문자열을 그대로 받는다.
탭 만들기
한페이지 안에 많은 정보를 한번에 보여줄 수 없으므로 Tab 기능을 많이 사용한다. 부트스트랩 예시 코드를 통해 살펴보자.
let [tab, setTab] = useState<number>(0);
<Nav variant="tabs" defaultActiveKey="link0">
<Nav.Item>
<Nav.Link onClick = {()=>{setTab(0)}} eventKey="link0">제품영양정보</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link onClick = {()=>{setTab(1)}} eventKey="link1">리뷰</Nav.Link>
</Nav.Item>
<Nav.Item>
<Nav.Link onClick = {()=>{setTab(2)}} eventKey="link2">교환</Nav.Link>
</Nav.Item>
</Nav>
클릭된 탭 정보는 상태로 관리한다. 또 defaultActiveKey 라는 속성은 기본적으로 눌려져 있는 탭의 이름을 뜻한다. 여기서는 link0이라는 키값을 가진 탭이 선택되었다.
이제 상태에 따라 정보를 선별적으로 보여주는 코드를 살펴보자.
const TabContent : FC<TabContentProps> = ({tab}) => {
if(tab == 0){
return <div>제품영양정보입니다.</div>
} else if(tab == 1){
return <div>리뷰관련 정보입니다.</div>
} else if(tab == 2){
return <div>교환/반품 관련 정보입니다.</div>
} else{
return null;
}
}
정보를 보여주는 컴포넌트이다. 삼항연산자를 사용해서 구현할 수도 있지만 지저분해 보이기 때문에 이렇게 적용하였다.
return [ <div>제품영양정보입니다.</div>, <div>리뷰관련 정보입니다.</div>, <div>교환/반품 관련 정보입니다.</div>][tab]
이와같이 배열을 이용해서 구현하면 더 간단하게 만들 수도 있다.
Context API
컴포넌트 간의 상태 공유는 현재 props를 통해 하고 있다. 하지만 컴포넌트가 많아지고 다층화될수록 번거로운 측면이 있다. 컴포넌트와 프롭스의 관계는 마치 함수와 매개변수와의 관계와도 같다. 그렇다면 기존 함수에서 사용하던 개념이 하나 떠오르는데 바로 전역변수이다. 컴포넌트에도 이런 비슷한 개념이 존재한다. 이를 Context Api 라고 한다.
interface ContextValue{
stock : number[];
}
export let contextStorage = createContext<ContextValue | undefined>(undefined);
먼저 createContext라는 함수를 통해 저장소를 생성해준다.
let [stock, setStock] = useState<number[]>([10,11,12]);
전역으로 사용할 상태를 선언하고,
<contextStorage.Provider value={{stock}}>
<Detail coffee = {coffee}></Detail>
</contextStorage.Provider>
tsx에서 사용한다. 이때 필요한 컴포넌트를 contextStorage.Provider 로 감싸주고, value 속성에 사용할 변수를 넣어준다.
그럼 사용하는 측에서의 코드는 어떨까?
import { contextStorage } from '../App';
import React, { useState, FC, useEffect, useContext } from 'react';
let ctx = useContext(contextStorage);
console.log(ctx);
useContext라는 함수를 사용해서 받아올 수 있다.
대신 useContext를 react에서 import 해주어야 하고 위해서 import 한 contextStorage도 import 해와야 한다.
let ctx = useContext(contextStorage);
console.log(ctx?.stock);
이렇게 ctx에 물음표를 붙일 수도 있는데 이것의 의미는 Optional 연산이다. 즉, stock 이라는 객체가 있다면 이것을 반환하지만 만약 없다면 undefined를 반환한다는 뜻이다.
또한 context api는 자식의 자식 컴포넌트에서도 마찬가지로 사용할 수 있다.
Context Api 단점
context api는 전역적으로 상태를 사용할 수 있게 해주지만 언듯 보면 props 보다 오히려 더 불편하게 느껴진다. 하지만 이는 컴포넌트 갯수가 적어서 그런 것이고 컴포넌트가 많아졌을 경우에 사용하면 좀 낫다.
또 하나의 단점은 한 컴포넌트에서 상태를 변경하면 관련된 모든 컴포넌트에서 재렌더링이 일어난다는 것이다. 성능에 안좋은 영향을 미칠 수 있다는 말이다.
마지막으로 매번 import 를 해주고 useContext를 해주어야 한다. 컴포넌트간의 의존성이 올라가게 되고 추후 재사용 등에 문제가 된다.
후기
useNavigate, 중첩 라우터, useEffect, Ajax 등등 많은 것들을 학습하였다.
키워드: 프로그래머스 데브코스, 국비지원교육, 코딩부트캠프
'프로그래머스 풀스택 데브코스 > 데브코스 TIL' 카테고리의 다른 글
웹 풀사이클 데브코스 TIL 56일차 (0) | 2024.02.11 |
---|---|
웹 풀사이클 데브코스 TIL 55일차 (0) | 2024.02.07 |
웹 풀사이클 데브코스 TIL 53일차 (0) | 2024.02.05 |
웹 풀사이클 데브코스 TIL 52일차 (0) | 2024.02.03 |
웹 풀사이클 데브코스 TIL 51일차 (0) | 2024.02.01 |