[React] SPA
React의 SPA에 대해 알아보자.
SPA
SPA란?
Single Page Application(싱글 페이지 에플리케이션)의 약자
한 개의 페이지로 이루어진 애플리케이션을 의미
↔️ 전통적 웹페이지는 여러 페이지로 구성됨
웹 페이지의 과거와 현재
과거
사용자가 다른 페이지로 이동 시 새 html을 받고, 페이지 로딩 마다 서버에서 리소스를 전달받아 해석한 뒤 화면에 출력
사용자에게 보이는 화면은 서버 츠깅 준비
* 사전에 html 파일을 만들어 제공 or 데이터 따라 유동적 html을 생성해주는 템플릿 엔진 사용
불필요한 로딩으로 인해 비효율적
현재
리액트 같은 라이브러리 혹은 프레임워크를 사용, 뷰 렌더링을 사용자의 브라우저가 담당
애플리케이션을 브라우저에 불러와서 실행 후 사용자와의 인터랙션 발생 시 필요한 부분만 자바스크립트를 사용하여 업데이트
* 새로운 데이터 필요시 서버 API 호출하여 필요 데이터만 새로 불러와 애플리케이션에서 사용 가능
싱글 페이지라고 해서 화면이 한 종류는 아니다.
ex) 블로그 개발 시 홈, 포스트 목록, 포스트, 글쓰기 등의화면이 필요
➡️ SPA의 경우 서버에서 사용자아게 제공하는 페이지는 한 종류지만, 해당 페이지에서 로딩된 자바스크립트와 현재 사용자 브라우저의 주소 상태에 따라 다양한 화면을 보여줄 수 있음
Routing(라우팅)
다른 주소에 다른 화면을 보여주는 것
브라우저의 API를 직접 사용하여 관리 or 라이브러리를 사용해 작업을 더욱 쉽게 구현 가능
리액트 라우팅 라이브러리
리액트 라우터(react-router), 리치 라우터(reach-router), Next.js 등
SPA의 단점
앱의 규모가 커지면 자바스크립트 파일이 너무 커짐
➡️ 페이지 로딩 시 사용자가 실제로 방문하지 않을 수도 있는 페이지의 스크립트도 불러오기 때문
▶️ 코드 스플리팅(code splitting)을 사용해 라우트별로 파일들을 나누어 트래픽과 로딩 속도 개선
브라우저에서 자바스크립트를 사용하여 라우팅을 관리하는 것은
자바스크립트를 실행하지 않는 일반 크롤러에서는 페이지의 정보를 제대로 수집해 가지 못한다는 잠재적 단점
▶️ 서버 사이드 레인더링(server-side rendering)을 통해 해결
라우터(router)
Create React App (CRA)에는 Routing을 위한 로직이 들어있지 않기 때문에
가장 인기 있는 routing solution인 react-router를 추가해서 routing을 구현
$ yarn add react-router-dom
라우터 적용
src/index.js 파일에서 react-router-dom에 내장되어 있는 BrowserRouter 컴포넌트를 사용하여 감싸면 된다.
BrowserRouter 컴포넌트
웹 애플리케이션에 HTML5의 History API를 사용하여 페이지를 새로 고침하지 않고도 주소 변경 및
현재 주소에 관련된 정보를 props로 쉽게 조회하거나 사용할 수 있도록 해줌
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
Route 컴포넌트로 특정 주소에 컴포넌트 연결
Route 컴포넌트를 사용하여 사용자의 현재 경로에 따라 다른 컴포넌트를 보여줄 수 있다.
Route 컴포넌트 사용 시 어떤 규칙을 가진 경로에 어떤 컴포넌트를 보여 줄 지 정의 가능
<Route path="주소 규칙" component={보여 줄 컴포넌트} />
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
function App() {
return (
<div>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
</div>
);
}
export default App;
서버 실행시 보여지는 Home 컴포넌트
주소창에 /about을 추가 입력하면
/about 경로로 들어갈 경우 About 컴포넌트만 나오는 것이 아니라 두 컴포넌트 모두 나타남.
/about 경로가 / 규칙에도 일치하기 때문에 발생한 현상
▶️ Home을 위한 Route 컴포넌트 사용 시 exact props를 true로 설정
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
function App() {
return (
<div>
<Route path="/" component={Home} exact={true} />
<Route path="/about" component={About} />
</div>
);
}
export default App;
Link 컴포넌트를 사용하여 다른 주소로 이동하기
Link 컴포넌트
클릭 시 다른 주소로 이동시켜 주는 컴포넌트
페이지 전환시, 페이지를 새로 불러오지 않고 애플리케이션은 그대로 유지한 상태에서 HTML5 History API를 사용하여 페이지의 주소만 변경
리액트 라우터 사용 시 a 태그를 직접 사용하지 않음.
➡️ a 태그는 페이지 전환 과정에서 페이지를 새로 불러오기 때문에 애플리케이션이 들고 있던 상태들을 모두 날려버림
➡️ 렌더링된 컴포넌트들도 모두 사라지고 다시 처음부터 렌더링
사용방법
<Link to="주소">내용</Link>
import React from 'react';
import { Link, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
function App() {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
</ul>
<Route path="/" component={Home} exact={true} />
<Route path="/about" component={About} />
</div>
);
}
export default App;
Route 하나에 여러 개의 path 설정하기
path props를 배열로 설정해 주면 여러 경로에서 같은 컴포넌트를 보여줄 수 있음.
import React from 'react';
import { Link, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
function App() {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
</ul>
<Route path="/" component={Home} exact={true} />
<Route path={["/about", "/info"]} component={About} />
</div>
);
}
export default App;
주소창에 /info를 입력해도 소개 페이지가 나타남.
URL 파라미터와 쿼리
페이지 주소 정의 시 유동적인 값을 전달해야 하는 경우 발생
▶️ 파라미터와 쿼리로 나눔
파라미터 또는 쿼리 사용은 무조건 따라야 하는 규칙은 없으나,
일반적으로 파라미터는 특정 아이디 혹은 이름을 사용하여 조회할 때 사용,
쿼리는 우리가 어떤 키워드를 검색하거나 페이지에 필요한 옵션 전달 시 사용
URL 파라미터
/profile/velopert와 같은 형식으로 뒷부분에 유동적인 username 값을 넣어 줄 때 해당 값을 props로 받아와서 조회하는 방법
Profile.js
import React from "react";
const data = {
velopert: {
name: "무녈",
description: "춘식이를 좋아하는 개발자",
},
teacher: {
name: "대니",
description: "음악을 때려친 개발자",
},
};
const Profile = ({ match }) => {
const { username } = match.params;
const profile = data[username];
if (!profile) {
return <div>존재하지 않는 사용자입니다.</div>;
}
return (
<div>
<h3>
{username}({profile.name})
</h3>
<p>{profile.description}</p>
</div>
);
};
export default Profile;
URL 파라미터 사용 시 라우트로 사용되는 컴포넌트에서 받아 오는 match라는 객체 안의 params 값을 참조함
match 객체 안에는 현재 컴포넌트가 어떤 경로 규칙에 의해 보이는지에 대한 정보가 내재
path 규칙에는 /profile/:username을 사용
match.params.username 값을 통해 현재 username 값 조회 가능
Router의 match, location, history 참고 객체
https://velog.io/@leo-xee/React-Router%EC%9D%98-match-location-history-%EA%B0%9D%EC%B2%B4
App.js
import React from "react";
import { Link, Route } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Profile from "./Profile";
function App() {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profile/velopert">velopert 프로필</Link>
</li>
<li>
<Link to="/profile/teacher">teacher 프로필</Link>
</li>
</ul>
<hr />
<Route path="/" component={Home} exact={true} />
<Route path={["/about", "/info"]} component={About} />
<Route path="/profile/:username" component={Profile} />
</div>
);
}
export default App;
URL 쿼리
쿼리는 Location 객체에 들어있는 search 값에서 조회 가능
location 객체
라우트로 사용된 컴포넌트에게 props로 전달되며, 웹 앱플리케이션의 현재 주소에 대한 정보를 지님.
search 값
URL 쿼리를 읽을 때는 위 객체가 지닌 값 중에서 search 값을 확인해야 함.
search는 문자열 형태로 구성
URL 쿼리는 ?detail=true&another=1과 같이 문자열에 여라 가지 값 설정 가능
search 값에서 특정 값을 읽어오기 위해 이 문자열을 객체형태로 변환해 주어야 함.
쿼리 문자열을 객체로 변환할 때 qs 라이브러리를 사용
$ yarn add qs
About 컴포넌트에서 location.search 값에 있는 detail이 true 여부에 따라 추가 정보를 보여 주도록 만들어보자.
import React from "react";
import qs from "qs";
const About = ({ location }) => {
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
// 이 설정을 통해 문자열 맨 앞의 ?를 생략
console.log(query)
const showDetail = query.detail === "true"; // 쿼리의 파싱 결과 값은 문자열
return (
<div>
<h1>소개</h1>
<p>이 프로젝트는 리액트 라우터 기초를 실습해 보는 예제 프로젝트입니다.</p>
{showDetail && <p>detail 값을 true로 설정하셨군요!</p>}
</div>
);
};
export default About;
* 쿼리를 사용할 때 쿼리 문자열을 객체로 파싱하는 과정에서 결과 값은 항상 문자열
서브 라우트
서브라우트
라우트 내부에 또 라우트를 정의하는 것
▶️ 라우트로 사용되고 있는 컴포넌트의 내부에 Route 컴포넌트를 또 사용하면 됨
두 종류의 프로필 링크를 잘라내서 프로필 링크를 보여주는 Profiles 라우트 컴포넌트를 생성하고,
그 안에서 Profile 컴포넌트를 서브 라우트로 사용하도록 코드 작성
Profiles.js
import React from "react";
import { Link, Route } from "react-router-dom";
import Profile from "./Profile";
const Profiles = () => {
return (
<div>
<h3>사용자 목록:</h3>
<ul>
<li>
<Link to="/profiles/velopert">velopert</Link>
</li>
<li>
<Link to="/profiles/teacher">teacher</Link>
</li>
</ul>
<Route
path="/profiles"
exact
render={() => <div>사용자를 선택해 주세요.</div>}
/>
<Route path="/profiles/:username" component={Profile} />
</div>
);
};
export default Profiles;
첫 번째 Route 컴포넌트에서 component 대신 render props를 사용
➡️ 컴포넌트 자체를 전달하는 것이 아니라, 보여주고 싶은 JSX를 넣어 줄 수 있다.
JSX에서 props 설정 시 값을 생략하면 자동으로 true로 설정
➡️ 첫 번째 Route에서 exact
App 컴포넌트에 Profiles 컴포넌트 추가
리액트 라우터 부가 기능
history 객체
라우트로 사용된 컴포넌트에 match, location과 함께 전달되는 props 중 하나로,
컴포넌트 내에 구현하는 매서드에서 라우트 API를 호출 가능
사용 예)
특정 버튼을 눌렀을 때 뒤로가기, 로그인 후 화면 전환, 다른 페이지로 이탈 방지
HistorySample.js
import React, { Component } from "react";
class HistorySample extends Component {
handleGoBack = () => {
this.props.history.goBack();
};
handleGoHome = () => {
this.props.history.push("/");
};
componentDidMount() {
// 설정 시 페이지에 변화가 생기려고 할 때마다 정말 나갈 것인지 질문함
this.unblock = this.props.history.block("정말 떠나실 건가요?");
}
componentWillUnmount() {
// 컴포넌트가 언마운트되면 질문을 멈춤
if (this.unblock) {
this.unblock();
}
}
render() {
return (
<div>
<button onClick={this.handleGoBack}>뒤로</button>
<button onClick={this.handleGoHome}>홈으로</button>
</div>
);
}
}
export default HistorySample;
App 컴포넌트에 HistorySample 컴포넌트 추가
withRouter
withRouter 함수는 HoC(Higher-order Components)
라우트로 사용된 컴포넌트가 아니어도 match, location, history 객체를 접근할 수 있게 해줌
WithRouterSample.js
import React from "react";
import { withRouter } from "react-router";
const WithRouterSample = ({ location, match, history }) => {
return (
<div>
<h4>location</h4>
<textarea
value={JSON.stringify(location, null, 2)}
// JSON.striginify의 두 번째 파라미터와 세 번째 파라미터를 위와 같이 설정시
// JSON에 들여쓰기가 적용된 상태로 문자열이 만들어짐
rows={7}
readOnly={true}
/>
<h4>match</h4>
<textarea
value={JSON.stringify(match, null, 2)}
rows={7}
readOnly={true}
/>
<button onClick={() => history.push("/")}>홈으로</button>
</div>
);
};
export default withRouter(WithRouterSample);
withRouter를 사용할 때 컴포넌트를 내보내기 위해 함수로 감싸준다.
Switch
Switch 컴포넌트는 여러 Route를 감싸서 그 중 일치하는 단 하나의 라우트만을 렌더링시켜줌.
Switch를 사용하면 모든 규칙과 일치하지 않을 때 보여 줄 Not Found 페이지도 구현 가능
import React from "react";
import { Link, Route, Switch } from "react-router-dom";
import Home from "./Home";
import About from "./About";
import Profiles from "./Profiles";
import HistorySample from "./HistorySample";
function App() {
return (
<div>
<ul>
<li>
<Link to="/">홈</Link>
</li>
<li>
<Link to="/about">소개</Link>
</li>
<li>
<Link to="/profiles">프로필</Link>
</li>
<li>
<Link to="/history">History 예제</Link>
</li>
</ul>
<hr />
<Switch>
<Route path="/" component={Home} exact={true} />
<Route path={["/about", "/info"]} component={About} />
<Route path="/profiles" component={Profiles} />
<Route path="/history" component={HistorySample} />
<Route
render={({ location }) => (
<div>
<h2>이 페이지는 존재하지 않습니다:</h2>
<p>{location.pathname}</p>
</div>
)}
/>
</Switch>
</div>
);
}
export default App;
NavLink
NavLink는 Link와 유사
현재 경로와 Link에서 사용하는 경로가 일치하는 경우 특정 스타일 혹은 CSS 클래스를 적용할 수 있는 컴포넌트
NavLink에서 링크가 활성화되었을 때의 스타일 적용 시 activeStyle 값을,
CSS 클래스 적용 시 activeClassName 값을 props로 전달
Profiles.js
import React from "react";
import { NavLink, Route } from "react-router-dom";
import Profile from "./Profile";
import WithRouterSample from "./WithRouterSample";
const Profiles = () => {
const activeStyle = {
background: "red",
color: "white",
};
return (
<div>
<h3>사용자 목록:</h3>
<ul>
<li>
<NavLink activeStyle={activeStyle} to="/profiles/velopert">
velopert
</NavLink>
</li>
<li>
<NavLink activeStyle={activeStyle} to="/profiles/teacher">
teacher
</NavLink>
</li>
</ul>
<Route
path="/profiles"
exact
render={() => <div>사용자를 선택해 주세요.</div>}
/>
<Route path="/profiles/:username" component={Profile} />
<WithRouterSample />
</div>
);
};
export default Profiles;
정리
리액트 라우터를 사용하여 주소 경로에 따라 다양한 페이지를 보여주는 방법을 공부하였다.
큰 규모의 프로젝트를 진행하다 보면 문제가 발생하는데, 웹 브라우저에서 사용할 컴포넌트, 상태 관리 로직, 그 외 여러 기능을 구현하는 함수들이 점점 쌓여 최종 결과물인 자바스크립트 파일의 크기가 매우 커진다는 것이다.
라우터에 따라 필요한 컴포넌트만 불러오고, 다른 컴포넌트는 다른 페이지를 방문하는 등의 필요한 시점에 불러오는 방법이 코드 스플리팅이며, 추후 학습하도록 한다.
참고