본문 바로가기
웹/24-StudyWithPnP

[React+JS/리액트] 웹 스터디 5주차

by 보먀 2024. 5. 23.
728x90
반응형

리액트 라우터: 기본 라우팅

 

html 과 js 를 사용해서 웹 페이지를 만들면 페이지 별로 html 이 존재하지만, 리액트에서는 한 개의 index.html 을 사용하여 웹페이지를 만든다. 이게 무슨 말이냐 하면 html 과 js 를 이용했을 때는 다른 페이지로 이동해야 해야하면 html 파일 자체를 바꾸어 줬지만(index.html -> detail.html 이런 식으로), 리액트는 index.html 파일은 놔두고 안에 내용을 바꾸어 준다는 것이다. 

 

 

리액트에서 페이지를 나눌 때 외부 라이브러리인 react-router-dom 이라는 외부 라이브러리를 설치해서 구현하는 것이 일반적이기 때문에 react-router-dom 을 설치해서 사용하기로 했다. 

 

react-router-dom 은 터미널을 열고 npm install react-router-dom 을 입력하면 된다.

(따로 원하는 버전이 있을 경우에는 npm install react-router-dom@버전 명령어를 사용하면 된다)

 

설치 후 몇 가지 셋팅이 더 필요한데, index.js 파일로 가서 BrowserRouter 를 import 해오고, <App /> 컴포넌트를 <BrowserRouter> 컴포넌트로 감싸주면 된다.

import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
      <BrowserRouter>
        <App />
      </BrowserRouter>
  </React.StrictMode>
);

 

 

 

- 라우터로 페이지 나누는 법

 

보통 웹 페이지를 보면 codingapple.com/A 는 A 페이지를 보여주고, codingapple.com/B 는 B 페이지를 보여준다. 

이렇게 url 경로에 따라 다른 페이지를 보여주려면 아래의 단계를 따르면 된다. 

 

1. <Routes>, <Route>, <Link> 컴포넌트 import 해오기

2. <Routes> 만들고 안에 <Route> 를 작성

3. <Route path="/url경로" element={ <보여줄 html> }

 

(참고로 path 에 "/" url 경로를 이렇게 적으면 웹 페이지에 처음 들어갔을 때 보이는 메인 페이지이다. )

import { Routes, Route, Link } from 'react-router-dom'

function App(){
  return (
    (생략)
    <Routes>
      <Route path="/" element={ <div>메인페이지임</div> } />
      <Route path="/detail" element={ <div>상세페이지임</div> } />
    </Routes>
  )
}

 

 

※ 참고

 

위의 코드에서는 element 안에 직접 html 코드를 집어넣었지만, 보여줘야 할 페이지의 코드가 길어지면 코드가 지저분해지기 때문에 다른 Js 파일을 만들어서 거기에 해당 페이지의 코드를 작성하고 import 해서 element 안에 집어넣는 식으로 코드를 짠다!

 

1. 새로운 js 파일 만들기

2. 보여줄 페이지 코드 짜기

3. App.js 파일로 돌아와서 새롭게 만든 파일 import

-> import 작명 from './~~/새로운파일.js'

4. <Route path="/새로운파일" element={ <작명></작명> }

 

 

- 페이지 이동 버튼

 

페이지를 이동하려면 페이지 이동 링크를 생성해야 한다. 여기에 필요한 것이 react-router-dom 의 <Link> 컴포넌트이다. 

아래의 코드처럼 <Link to='url 경로'>~~</Link> 를 사용하면 된다. 

<Link to="/"></Link>
<Link to="/detail">상세페이지</Link>

 

 

숙제

1. /detail 로 접속했을 때 보여줄 상세페이지를 컴포넌트를 이용해서 만들기 + 코드가 길어졌으니 다른 파일로 만들어서 작성

// detail.js
import React from "react";

function Detail() {
    return (
        <div className="container">
            <div className="row">
                <div className="col-md-6">
                    <img src="https://codingapple1.github.io/shop/shoes1.jpg" width="100%" />
                </div>
                <div className="col-md-6">
                    <h4 className="pt-5">상품명</h4>
                    <p>상품설명</p>
                    <p>120000원</p>
                    <button className="btn btn-danger">주문하기</button> 
                </div>
            </div>
        </div> 
    );
}

export default Detail;
// App.js
import './App.css';
import { Col, Row, Container, Nav, Navbar } from 'react-bootstrap';
import React, { useState } from 'react';
import data from './data.js';
import Items from './items.js'
import Detail from './detail.js';
import { Routes, Route, Link } from 'react-router-dom'

function App() {

  return (
    <div className="App">
      <Link to="/"></Link>
      <Link to="/detail">상세페이지</Link>
      <Navbar bg="dark" data-bs-theme="dark">
          <Container>
          <Navbar.Brand href="#home">Navbar</Navbar.Brand>
          <Nav className="me-auto">
            <Nav.Link href="#home">Home</Nav.Link>
            <Nav.Link href="#features">Features</Nav.Link>
            <Nav.Link href="#pricing">Pricing</Nav.Link>
          </Nav>
        </Container>
      </Navbar>


       <Routes>
        <Route path='/' element={
          <>
            <div className='main-bg'></div>
              <div>
                <Container>
                  <Items></Items>
                </Container>
              </div>
          </>
        }></Route>
        <Route path='/detail' element={<Detail></Detail>}></Route>
      </Routes>
    </div>
  );
}

export default App;

 

 

리액트 라우터2: navigate, nested routes, outlet

 

저번 강의에서 라우터로 페이지를 나누는 법을 배웠는데, 그렇다면 페이지가 많아졌을 때 프로젝트의 폴더 관리는 어떻게 해야 할까?

비슷한 파일끼리 묶어서 관리하면 관리하기 쉽다.

 

컴포넌트 역할을 하는 js 파일은 components 폴더에 묶고,

페이지 역할을 하는 js 파일은 routes 아니면 pages 폴더에 묶고,

자주 쓰는 함수가 들어있는 js 파일은 utils 폴더에 묶는 식으로 관리하면 좋다. 

 

 

- 페이지 이동기능 만들기: useNavigate()

 

useNavigate 도 react-route-dom 을 import 해서 사용할 수 있다. useNavigate() 를 쓰면 페이지를 이동할 수 있게 해주는 함수를 반환한다. 그 함수를 navigate 변수에 저장하고 이동을 원하는 라우터의 path 를 적어주면 이동할 수 있다. 

function App(){
  let navigate = useNavigate()
  
  return (
    <Routes>
       <Route path='/detail' element={ <Detail /> } />
    </Routes>
    <button onClick={()=>{ navigate('/detail') }}>이동버튼</button>
  )
}

 

 

 

navigate(숫자) 를 사용하면 앞으로가기 / 뒤로가기 기능을 사용할 수 있다. 

숫자가 양수인 경우 숫자만큼 앞으로 가고, 숫자가 음수인 경우 숫자만큼 뒤로갈 수 있다.

// 양수는 앞으로 가기
<button onClick={()=>{ navigate(1) }}>이동버튼</button>
<button onClick={()=>{ navigate(2) }}>이동버튼</button>

// 음수는 뒤로 가기
<button onClick={()=>{ navigate(-1) }}>이동버튼</button>
<button onClick={()=>{ navigate(-2) }}>이동버튼</button>

 

 

 

- 404 페이지 만들기

 

잘못된 url 로 접근했을 때 404 에러가 난다. 유저가 이상한 경로로 접속했을 때 존재하지 않는 페이지라는 것을 보여주고 싶으면 아래와 같이 코드를 짜고 라우팅시켜주면 된다. * 는 모든 경로를 뜻하기 때문에 만들어둔 경로가 아닌 이상한 경로를 접속했을 때 * 경로로 안내해준다.

 <Route path="*" element={ <div>없는페이지임</div> } />

 

 

 

- 서브 경로 만들기: nested routes

 

/about 페이지가 있고 /about/member 접속시 회사 멤버 소개 페이지, /about/location 페이지 접속시 회사 위치를 소개하는 페이지를 만들고 싶다면 nested routes 를 사용하면 된다. <Route> 안에 <Route> 를 넣을 수 있는데, 이걸 Nested routes 라고 부른다. 

<Route path="/about" element={ <About/> } >  
  <Route path="member" element={ <div>멤버들</div> } />
  <Route path="location" element={ <div>회사위치</div> } />
</Route>

 

 

여기서 의문 생기는데, nested routes 는 그냥 <Route> 를 병렬적으로 나열하는 것과 뭐가 다르길래 이렇게 사용하는 것일까?

nested routes 는 감싼 <Route> 와 안에 있는 <Route> 를 같이 보여줄 수 있다. 

 

감싼 <Route> 의 컴포넌트 <About> 안에 <Outlet></Outlet> 을 넣어준 자리에 nested routes element 들이 보여진다. 

 

- /about/member 접속시 <About> & <div>멤버들</div>

- /about/location 접속시 <About> & <div>회사위치</div> 

<Route path="/about" element={ <About/> } >  
  <Route path="member" element={ <div>멤버들</div> } />
  <Route path="location" element={ <div>회사위치</div> } />
</Route>

function About(){
  return (
    <div>
      <h4>about페이지임</h4>
      <Outlet></Outlet>
    </div>
  )
}

 

 

그래서 유사한 서브페이지들이 많이 필요하다면 페이지 전체를 렌더링해줄 필요없이 nested routes 를 사용하면 일부만 갈아치워서 보여줄 수 있기 때문에 유용하다. 

 

 

숙제: 아래의 페이지들을 nested routes 써서 만들어보기

 

- /event/one 페이지로 접속하면 하단처럼 생긴 페이지

- /event/two 페이지로 접속하면 하단처럼 생긴 페이지

<Routes>
  <Route path="/event" element={<Event />}>
    <Route path="one" element={<p>첫 주문시 양배추즙 서비스</p>}></Route>
    <Route path="two" element={<p>생일기념 쿠폰받기</p>}></Route>
  </Route>
</Routes> 

function Event(){
  return (
    <div>
      <h4>오늘의 이벤트</h4>
      <Outlet></Outlet>
    </div>
  )
}

 

 

 

- 상세페이지에 상품명 넣기 (데이터 바인딩)

 

shoes 라는 state 에 있던 상품정보들을 Detail 컴포넌트에 데이터 바인딩 시켜보려고 한다. shoes 는 App 컴포넌트에 있기 때문에 Detail 컴포넌트에서 사용하려면 props 를 통해 전송받고 등록해서 사용하면 된다.

// App.js
<Route path="/detail" element={ <Detail shoes={shoes}/> }/>

 

 

// Detail.js
<div className="container>
  <div className="row">
    <div className="col-md-6">
      <img src="https://codingapple1.github.io/shop/shoes1.jpg" width="100%" />
    </div>
    <div className="col-md-6 mt-4">
      <h4 className="pt-5">{props.shoes[0].title}</h4>
      <p>{props.shoes[0].content}</p>
      <p>{props.shoes[0].price}원</p>
      <button className="btn btn-danger">주문하기</button>
    </div>
  </div> 
</div>

 

 

※ 참고

shoes 라는 state 를 Detail.js 안에 만들어 사용하면 props 를 통해 전송 받지 않아도 되는데, 굳이 전송 받아서 사용하는 이유는 수정이 필요할 경우 데이터가 여러 군데 있으면 수정이 어렵기 때문이다. 가급적 데이터는 한군데 두고 사용하는 것이 좋다. 

 

 

- 상세페이지 여러 개 만들기

 

위의 코드는 0번 상품만 보여주는 상세페이지이다. 상세페이지가 적다면 <Route> 를 여러개 만들어서 상세페이지를 여러 개 만들어줘도 상관없지만, 만들어야 할 상세페이지가 많은 경우에는 다른 방법을 사용하는 것이 좋다. 

 

바로 URL 파라미터 문법을 사용하는 것이다. 

path 작명할 때 /: 어쩌고 이렇게 사용하면 "아무 문자" 를 뜻한다. 그래서 아래의 <Route> 는 유저가 주소창에 /detail/아무거나 입력했을 때 <Detail> 컴포넌트를 보여달라는 뜻이 된다. 

<Route path="/detail/:id" element={ <Detail shoes={shoes}/> }/>

 

 

그럼 사용자가 "아무거나" 를 유의미하게 만들어보자. 

사용자가 입력한 값에 따라 보여주는 상세페이지가 달라지도록 만들면 된다. 

 

useParams() 함수를 사용하면 현재 /:url파라미터 자리에 유저가 입력한 값을 가져올 수 있다. 아래와 같이 코드를 짜면 받아온 url 파라미터 값을 활용해서 파라미터에 따라 다른 상세페이지를 보여줄 수 있다. 

// Detail.js
import { useParams } from 'react-router-dom'

function Detail(){
  let {id} = useParams();
  
  return (
    <div className="container>
      <div className="row">
        <div className="col-md-6">
          <img src="https://codingapple1.github.io/shop/shoes1.jpg" width="100%" />
        </div>
        <div className="col-md-6 mt-4">
        <h4 className="pt-5">{props.shoes[id].title}</h4>
        <p>{props.shoes[id].content}</p>
        <p>{props.shoes[id].price}원</p>
        <button className="btn btn-danger">주문하기</button>
      </div>
    </div>
  </div>
  )
}

 

 

※ 참고 - url 파라미터는 몇 번이고 사용 가능

detail/:id/:email/:name -> 이런 식으로 몇 번씩 사용가능하다!

 

 

응용문제

Q. shoes state 의 순서가 변경되면 같은 url 파라미터를 통해 접속해도 다른 상세페이지가 보이는 문제는 어떻게 해결?

 

let shoes = useState([nike, addidas, keen]) 의 경우

/detail/0 접속시 nike 

 

state 의 순서가 바뀌어

let shoes = useState([addidas, keen, nike]) 의 경우

/detail/0 접속시 addidas

 

이렇게 상세페이지가 불규칙해지는 문제를 어떻게 해결하면 좋을까?

function Detail(props){

  let { id } = useParams();
  let 찾은상품 = props.shoes.find(function(x){
    return x.id == id
  });

  return (
    <div className="container">
      <div className="row">
        <div className="col-md-6">
          <img src="https://codingapple1.github.io/shop/shoes1.jpg" width="100%" />
        </div>
        <div className="col-md-6 mt-4">
          <h4 className="pt-5">{찾은상품.title}</h4>
          <p>{찾은상품.content}</p>
          <p>{찾은상품.price}원</p>
          <button className="btn btn-danger">주문하기</button> 
        </div>
      </div>
  </div>  
  )
};

export default Detail

 

함수를 만들어서 shoes 데이터에 있는 각 데이터 고유 id 와 url 파라미터 값과 비교해서 같은 값을 찾아서 상세페이지를 보여준다. 

 

1. find 함수는 array 뒤에 붙일 수 있고, return 조건식을 적으면 조건식에 맞는 자료를 남겨준다. 

2. find 콜백함수에 파라미터를 넣으면 array 자료에 있던 자료를 뜻한다.

3. x.id == id 라는 조건식을 써서 array 자료와 url에 입력한 id 가 같은 경우 결과를 변수에 담아준다.

이렇게 하면 조건에 맞는 상품이 {상품1개} 이런 식으로 담긴 객체를 리턴한다. 

 

 

※ 참고

4가지 코드 모두 비슷해보이지만 3 번 코드처럼 함수를 작성하면 에러가 발생한다. 

 

4번 함수처럼 화살표 함수에서 중괄호 {} 없이 작성된 표현식은 해당 표현식의 결과값을 반환한다. 따라서 x.id == id 의 결과가 true 또는 false 가 되며, find 메서는 true 를 반환하는 첫 번째 요소를 찾는다. 

 

3번 함수처럼 화살표 함수에서 중괄호를 사용하면 중괄호 안의 코드는 블록으로 취급되며, 명시적으로 return 키워드를 사용하지 않으면 아무것도 반환하지 않는다. 따라서 이 함수는 항상 undefined 를 반환하게 된다. find 메서드는 true 를 반환하는 요소를 찾는데,  명시적으로 return 을 해주지 않아 undefind 를 반환하게 되어 원하는 결과를 얻지 못하게 한다. 

// 1
let { id } = useParams();
let 찾은상품 = props.shoes.find(function(x){
    return x.id == id
});

// 2
let { id } = useParams();
let 찾은상품 = props.shoes.find((x) => {
    return x.id == id
});

// 3 -> X
let { id } = useParams();
let 찾은상품 = props.shoes.find((x) => { x.id == id });

// 4
let { id } = useParams();
let 찾은상품 = props.shoes.find((x) => x.id == id);

 

 

 

styled-components 

 

컴포넌트가 많은 경우 스타일링을 하다보면 아래와 같은 불편한 상황들이 생기는데, styled-components 라는 라이브러리를 사용하면 불편한 상황을 없앨 수 있다. 

 

1. class 만들어놓은걸 까먹고 중복해서 또 만들거나

2. 갑자기 다른 이상한 컴포넌트에 원하지 않는 스타일이 적용되거나

3. CSS 파일이 너무 길어져서 수정이 어렵거나

 

 

- styled-components 설치

 

터미널을 열고 npm install styled-components 를 입력하여 설치하고, 사용하고 싶은 컴포넌트에서 import 하여 사용하면 된다.

import styled from 'styled-components'

 

 

 

- styled-components 기본적인 사용법

 

이 라이브러리를 이용하면 컴포넌트를 만들 때 스타일을 미리 주입해서 만들 수 있다. 

 

1. 원하는 태그를 만드려면  -> styled.원하는 태그 

2. 오른쪽에 백틱 기호를 이용해서 CSS 스타일을 넣으면 됨

3. 반환된 컴포넌트를 변수에 저장해서 쓰면 됨

4. 저장한 변수이름을 컴포넌트처럼 쓰면 됨

 

예시로 padding: 20px, color: grey 인 div 박스와 padding: 10px, color: black, padding: 10px 인 button 을 만들어보자.

import styled from 'styled-components';

let Box = styled.div`
  padding : 20px;
  color : grey
`;
let YellowBtn = styled.button`
  background : yellow;
  color : black;
  padding : 10px;
`;

function Detail(){
  return (
    <div>
      <Box>
        <YellowBtn>버튼임</YellowBtn>
      </Box>
    </div>
  )
}

 

 

 

- 추가 문법: props 로 재활용 가능

 

만약 비슷한 UI 가 필요한 경우에는 비슷한 컴포넌트를 여러 개 만들지 말고 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>
  )
}

 

 

참고1 - 아래와 같이 간단한 프로그래밍이 가능하다.

let YellowBtn = styled.button` 
  background : ${ props => props.bg };
  color : ${ props => props.bg == 'blue' ? 'white' : 'black' };
  padding : 10px; 
`;

 

참고2 - 기존 스타일 복사 가능

let YellowBtn = styled.button`
    background: yellow;
    padding: 10px;
`

let NewBtn1 = styled.button(YellowBtn);

// `` 사용해서 스타일 추가 가능
let New2 = styled.button(YellowBtn)`
    color: black;
`

 

 

styled-components 의 장점

1. CSS 파일을 오픈할 필요없이 JS 파일에서 바로 스타일을 넣을 수 있다

2. 파일 안에 적은 스타일은 그 파일 안에서만 사용되기 때문에 다른 JS 파일로 오염되지 않는다. (원래 CSS 파일은 오염된다)

3. 파일에 적은 스타일은 html 페이지에 <style> 태그에 적어주는데, 해당 페이지와 관련있는 페이지와 관련된 CSS 만 로딩하면 되기 때.      문에 페이지 로딩시간이 적다

 

 

styled-components 의 단점

1. JS 파일 매우 복잡해짐

2. 중복스타일은 결국 컴포넌트 간 import 를 사용해야 하므로 CSS 파일과 다를 바 없다

3. 협업시 CSS 담당의 숙련도 이슈

 

 

 

- 컴포넌트명.module.css 

 

사실 일반 CSS 파일을 사용해도 오염 방지가 가능하다.

 

App.css 파일을 만들어서 App.js 파일에서 import 해서 사용하면 다른 JS 파일에서도 사용가능하기 때문에 오염이 되는데, 프로젝트 사이즈가 커지면 오염으로 관리하기 힘들어진다. 

 

그럴 땐 styled-components 를 써도 되지만 그냥 CSS 파일에서 제공하는 다른 js 파일에 간섭하지 않는 '모듈화' 기능을 사용하면 된다. 사용방법은 간단하다. 컴포넌트명.module.css 라고 CSS 파일의 이름을 짓고 컴포넌트명.js 파일에서 import 해서 사용하면 그 스타일들은 컴포넌트명.js 파일에만 적용된다. 

728x90
반응형