[회고][프로젝트] React로 쇼핑몰에 라우팅과 API 통신 구현해보기

Gyuwon Lee·2022년 7월 26일
0

회고

목록 보기
4/5

코딩애플의 쇼핑몰 프로젝트 결과물에 대한 회고입니다.

리액트를 기반으로 axios 및 리액트 라우터, redux를 사용하여 장바구니 기능을 구현한 쇼핑몰 웹사이트에 대한 회고를 정리했습니다.

🌟 왜 시작했나요?

HTML과 CSS로 웹 페이지를 만들어 보고, JS를 사용하여 CRUD 기능을 구현한 일전의 두 프로젝트를 통해 프론트엔드 개발의 기초적인 부분들을 학습했습니다. 하지만 이는 모두 한 개의 페이지로 구성되어 라우팅이 필요없는 어플리케이션이라는 점이 한계점으로 느껴졌습니다.

또한 이번에는 리액트를 바탕으로 새로운 라이브러리 및 도구들을 학습하여 프론트엔드 역량을 한 단계 높이고 싶다는 생각도 들었습니다. 이전 프로젝트에서는 HTML, CSS, JS로 파일을 나누어 페이지를 구성했지만, 현재 대부분의 프론트엔드 개발은 위의 세 요소를 하나의 모듈로 캡슐화한 웹 컴포넌트를 기반으로 이루어지고 있기 때문입니다.

따라서 본 프로젝트를 통해, 직접 쇼핑몰 웹사이트를 만들며 컴포넌트 기반 개발을 경험해보고 상태관리, 라우팅, API 통신 등 프론트엔드 개발의 핵심적인 역량들을 빠르게 학습하고자 했습니다.

🌱 뭘 배웠나요?

React-Bootstrap 라이브러리

본 프로젝트에서는 스타일링을 위해 React-Bootstrap을 사용했습니다. 부트스트랩 라이브러리는 패스트캠퍼스 강의 중 html에 CSS CDN을 연결해 사용해본 경험이 있기 때문에 낯설지 않았지만, 리액트 패키지로 사용해본 것은 처음이었습니다.

Stylesheets 불러오기

html 또는 js 파일에 포함할 수 있는데, 본 프로젝트에서는 사이트의 진입점인 index.html에서 스타일 관련 코드를 관리하고자 html 파일에 포함시켰습니다.

컴포넌트 임포트

import Button from 'react-bootstrap/Button';

// or less ideally
import { Button } from 'react-bootstrap';
  • 라이브러리 전체를 임포트하기보다, 필요한 컴포넌트만 독립적으로 불러오는 것이 권장된다. 불필요한 코드까지 전부 불러오는 것을 방지하기 위해서다.

import , export

[React]import, export 사용 방법 게시물에 많은 도움을 받았습니다.

ESM

ES6 부터 지원하고 있는 표준 모듈 시스템으로, 이를 통해 import와 export 문법을 사용하여 모듈을 불러오거나 내보낼 수 있게 되었다.

ES6 이전까지는 브라우저 환경에서 사용할 수 있는 표준 모듈 시스템이 없었기 때문에, 필요한 파일(모듈)을 만들어서 같이 배포하고, <script src=”script.js”> 의 형태로 파일을 직접 불러오는 방법을 사용했다.

<script> 만으로는 크고 복잡한 시스템에서 사용되는 수많은 파일들을 효율적으로 관리하기가 어렵다는 단점이 있었기 때문에, 새로운 모듈 시스템이 도입된 것이다.

import React from 'react';
import { render } from 'react-dom';
...
// export { foo as default, bar };
export default foo;
export { bar };

ESM으로 모듈 정리하기

export-from 은 import와 export를 한 번에 처리할 수 있는 문법이. 이 문법은 주로 패키지의 다른 모듈들을 한 번에 모아서 일관된 형태로 내보내거나 관리하고 싶은 경우에 사용할 수 있다.

// src/utils/index.js
export { add } from './add';
export { subtract } from './subtract'
export * from 'formulas';
// src/index.js
// 디렉토리를 import하면 기본적으로 index.js 파일을 탐색함
import * as utils from './utils/'

이렇게 src/utils/ 패키지의 index 에서는 모듈을 한 번 정리해 주고, 이렇게 정리한 모듈을 하나의 모듈로 내보내서 필요한 메서드와 변수 등을 사용할 수 있다. 따라서 각 경로별로 여러번 import 되었어야 하는 코드가 단 한줄로 처리된다.

리액트 라우터: 공식문서

라우팅을 위해 리액트 라우터를 사용하며, 먼저 공식문서를 읽고 일부 내용을 정리해 보았습니다.

리액트 라우터의 장점

  • 리액트 라우터를 사용하면, 앱 내의 라우팅을 History API 와 같은 브라우저 내장 API와 연동시킬 수 있다.
    - 이에 따라 컴포넌트들에 주소를 부여하고, URL을 기반으로 라우팅해서 컴포넌트 간 앞으로 가기 / 뒤로 가기, 북마크, 현재 위치 기억(새로고침 해도 동일한 페이지 표시) 등이 가능해진다.
    - History API - MDN 참고

리액트 라우터를 사용하는 이유

사실 리액트 라우터를 꼭 사용하지 않아도 라우팅 구현은 어렵지 않다.

  • 간단한 예로, 각 페이지로 연결된 이동 버튼을 만들어 클릭 등의 이벤트 발생 시 해당 페이지(컴포넌트)로 갱신되게 할 수도 있다.

하지만 위와 같은 방식의 라우팅은 조건에 따라 각기 다른 컴포넌트를 렌더링할 뿐, 사이트의 URL에 관여하지 못한다. 즉, 페이지 및 내용은 변하고 있지만 브라우저상의 URL은 그대로인 상태다. 이로 인한 문제는 다음과 같은 것들이 있다:

  • 특정 페이지에 대한 즐겨찾기 등록이 불가하다. 컴포넌트가 전환되더라도 브라우저 주소창의 URL은 고정되어 있기 때문이다.

  • 뒤로 가기 버튼을 누르면 해당 앱내에서 이전 페이지로 이동하는 것이 아니라 그 전에 서핑하던 다른 웹사이트로 이동해버린다.

  • 새로 고침 버튼을 누르면 사용 중이던 컴포넌트가 아닌 무조건 최초에 렌더링되었던 Home 컴포넌트로 이동한다.

페이지 나누기 - 리액트 미사용 ver.

  1. html 파일을 새로 만들어서 해당 페이지 내용을 구성
  2. 누군가 /하위경로로 접속하면 새로 만든 html 파일을 보냄

페이지 나누기 - 리액트 라우터 ver.

  • 리액트는 기본적으로 Single Page Application으로, html 파일은 오직 하나만 사용한다.
  1. 컴포넌트를 만들어서 해당 페이지 내용을 구성하고
  2. 누군가 /하위경로로 접속하면 새로 만든 컴포넌트를 보냄

Router - BrowserRouter

리액트 라우터는 크게 RouterComponents 로 나뉜다.

  • Router 는 브라우저상의 URL과 리액트 앱을 연결해주는 역할을 한다.

  • ComponentsRouter 에 의해 구분되는 각각의 페이지 경로를 나타낸다.

Router는 BrowerRouter 외에도 HashRouter, HistoryRouter 등 여러 종류가 지원되는데, 공식 문서에 따르면 웹브라우저 상에서 React Router 를 사용할 때 추천되는 인터페이스가 BrowserRouter 다.

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

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

Components - Routes and Route

브라우저에서 유저가 페이지를 이동한 내역은 브라우저의 History Stack 에 저장되는데, 이 때 window.location 객체에 기반한 값이 사용된다. 이 객체는 URL의 객체 표현에 가깝지만 부가적인 내용을 더 담고 있으며, 말 그대로 "유저가 어느 위치에 있는지"를 나타낸다.

React Router의 Components 중 RoutesRoute 는 이 location 에 기반하여 무언가 렌더링하고자 할 때 사용되는 가장 기본적인 방식이다.

  • 공식문서에 따르면 Route 를 마치 if 조건문과 같은 원리로 이해할 수 있다고 한다.

    • 만약 특정 Routepath prop이 현재 URL(location)과 일치한다면, element prop의 엘리먼트를 렌더링해준다.
  • location 이 변화할 때마다, Routes 는 자신이 감싸고 있는 자식 Route 들을 모두 탐색하여 현재 브라우저상의 location 과 가장 일치하는(best match) 것을 찾고 해당 UI를 렌더링해준다.

Components - Outlet

[React]React Router v6 기초 학습[React-Router v6] Nested Routes 게시물을 참고했습니다.

중첩 라우팅 (Nested Routing) 이 필요할 때는 Outlet 컴포넌트를 사용해야 한다.

예를 들어, https://www.main.com/sub1https://www.main.com/sub2 사이를 라우팅하는 것은 어렵지 않다. 앞서 보았듯 Router 에 의해 현재 location 과의 best match 를 찾아서 렌더링하면 되기 때문이다.

반면 https://www.main.com/sub1https://www.main.com/sub1/1, https://www.main.com/sub1/1/details 등등 경로가 점점 깊어지는 경우 어떻게 라우팅할 것인가?

이러한 하위 페이지까지 모두 최상위 컴포넌트의 Routes 내부에 넣는다면, Route 들 만으로 코드는 걷잡을 수 없이 길어질 것이다.

이에 따라 각 하위 컴포넌트 레벨에서 (위 예시의 경우 /sub1 ) 하위 경로에 대한 라우팅을 모듈화하고자 고안된 것이 중첩 라우팅이며, Outlet 컴포넌트를 사용하는 이유다.

  • Outlet 컴포넌트를 사용하면 페이지 전체를 새로 렌더링하는 것이 아니라 해당 부분의 UI만 (nested UI) 변경시키면서 URL역시 변경시킬 수 있다.

useNavigate

useNavigate() 를 사용하면 임의의 요소에 onClick 이벤트 핸들러를 달아 특정 페이지로 이동하도록 할 수 있다.

let navigate = useNavigate();

<button onClick={() => { navigate(1) }}>prev</button>
<button onClick={() => { navigate(-1) }}>next</button>

URL 파라미터

URL 파라미터와 쿼리

  • 파라미터 : /details/0
    - 일반적으로 파라미터는 특정 id나 이름을 가지고 조회할 때 사용

  • 쿼리 : /details?color=red
    - 쿼리의 경우엔 어떤 키워드를 검색하거나, 요청을 할 때 필요한 옵션을 전달할 때 사용

useParams 로 URL 파라미터 받기

// App.js

<Route path="/details/:id" element={ <Detail products={products} /> } />
// Detail.js

let {id} = useParams();
  • 파라미터의 위치는 Route 태그의 path 속성 안에 경로명/:파라미터변수명 으로 밝혀 준다.
    • path 작명 시 : 는 '아무 문자' 를 뜻한다.
  • 파라미터를 사용할 컴포넌트 내부에서 변수에 useParams 를 저장한다.
    • 이 때 useParams() 로 전달된 파라미터는 String 자료형이다. 추후 정수 값 등과 비교하기 위해서는 형 변환을 하든가, 일치 연산자 (===) 대신 동등 연산자(==)를 사용한다.

styled-components

컴포넌트가 늘어날수록, 스타일링을 하다 보면 불편함이 생긴다 :

  • class 만들어둔 걸 까먹고 중복해서 또 만들거나
  • 갑자기 다른 컴포넌트에 의도하지 않은 스타일이 적용되거나
  • CSS 파일이 너무 길어져서 수정이 어렵거나...

styled-components 라이브러리를 사용하면, 스타일을 바로 입혀서 컴포넌트를 만들 수 있다.

장점

  • CSS 파일을 오픈할 필요 없이 JS 파일 내부에서 바로 스타일을 넣을 수 있다.
  • 한 파일에 적용한 스타일이 다른 JS 파일로 오염되지 않는다. 일반 CSS 파일은 바벨이 트랜스파일하는 과정에서 하나로 합쳐지며 오염을 야기할 수 있다.
  • 페이지 로딩 시간이 단축된다.

단점

  • CSS 길이를 줄이려다 되려 JS 파일 길이를 복잡하게 늘리는 결과를 낳을 수 있다.
  • JS 파일 간 중복 디자인이 많이 필요한 경우, 다른 파일에서 import 해와서 사용해야 한다.
    - 이 때는 오히려 한 CSS 파일에 스타일을 두고 파일들이 공유할 수 있도록 하는 방식이 나을 것이다.
  • CSS를 담당하는 디자이너와 협업하는 경우에는, 디자이너의 styled-components 에 대한 이해도에 따라 다시 CSS로 변환하거나 반대로 CSS 코드를 JS의 styled 문법으로 옮겨오는 등의 번거로움이 발생한다.
  • 새로운 기술(라이브러리)를 도입할 땐, 나 혼자 간편하겠다고 쓰는 게 아니라 확장성 및 팀원들의 이해도를 고려하여 결정해야 한다

컴포넌트의 Lifecycle과 useEffect

  • mount : 컴포넌트가 화면에 로드됨(보임) = 페이지에 장착됨
  • update : 컴포넌트가 변화(업데이트)됨 (state 조작 등)
  • unmount : 컴포넌트가 필요없으면 제거됨 (state 조작, 페이지 이동 등)

lifecycle을 파악하고 있으면, 특정 시점에 프로그래머가 임의로 간섭할 수 있다.

useEffect 왜 쓰나요?

useEffect 안에 있는 코드는 html 렌더링 후에 동작한다. 즉, html요소들을 화면에 다 띄워주고 나서 useEffect 내부의 코드를 실행한다.

import useEffect from 'react'

function foo() {
	useEffect (() => { 
		for (var i = 0; i < 10000; i++)
			console.log(1);
	})

	return (
		<div>html 요소</div>
	)
}

위와 같은 코드에서, 10000번 돌아가는 for 반복문은 약 1~2초의 딜레이를 발생시킬 수 있다. 만약 useEffect 안에 적지 않았다면, 자바스크립트 엔진은 코드를 위에서부터 순차적으로 실행시키므로 for문이 끝날때까지의 1~2초동안 사용자는 화면상에서 어떤 html요소도 볼 수 없다.

일단 화면상에 뭐라도 보이는 것과 빈 화면이 띄워져 있는 것은 사용자의 성능 체감을 크게 좌우한다.

  • 어려운 연산
  • 서버에서 데이터 가져오는 작업
  • 타이머 설정

등등의 코드들은 useEffect 내부에 작성함으로써 UX을 개선할 수 있다.

여담: useEffect의 어원

  • Side Effect (부작용 아님) : "함수의 핵심기능과 상관없는 부가기능"
    • 프론트에서 함수의 핵심기능이란? html 렌더링
    • useEffect = Side Effect 코드들 보관함

서버와 Ajax

서버?

  • 데이터를 요청받고, 데이터를 보내주는 프로그램
    • Youtube 서버 : 동영상 요청하면 진짜 보내주는 프로그램
    • 네이버웹툰 서버 : 웹툰 요청하면 진짜 보내주는 프로그램
  • 서버에 데이터는 어떻게 요청하는가?
    • 규격에 맞춰서 요청한다 == API

Ajax

  • 서버에 GET, POST 요청을 할 때 새로고침 없이 데이터를 주고받는 브라우저 기능
    • 새로고침 없이 쇼핑몰 상품 더보기
    • 새로고침 없이 댓글 서버로 전송 및 다시 DOM에 반영하기
  • fetch() (JS 메소드), axios (외부 라이브러리) 등등

Context API

성능 이슈 등의 문제가 있어 현업에서 잘 사용되지는 않지만, 리액트 기본문법 이라서 외부 라이브러리가 필요없다는 장점이 있다.

Context API 왜 잘 안쓰나요?

  1. 성능 이슈
    • Context 태그로 둘러싼 내부의 모든 자식(자손)컴포넌트들은, Context 태그의 value로 넘겨진 state가 업데이트될때마다 전부 재렌더링된다.
    • 문제는 그 state를 사용하지 않는 컴포넌트들도 무조건 재렌더링된다는 점
    • 컴포넌트 중첩 구조가 깊어질수록 성능 이슈를 발생시킨다.
  2. 컴포넌트 재사용이 어렵다.
    • 만약 위 코드의 Detail 컴포넌트를 다른 페이지(파일)에서 재사용하려면?
    • Detail.js 안에 있는 Context1이나 stock, products 는 다른 페이지로 넘겨지지 않기 때문에 문제가 생길 수 있다.

🛠 겪은 문제와 해결

문제

  • App.js 안에서 컴포넌트를 만들고 중첩 라우팅을 시도하면 잘 되는데, 모듈을 분리하면 페이지가 표시되지 않는 문제를 겪었습니다.

궁금증

  1. 분리된 모듈에서 “/“ 경로는 정확히 어떤 페이지를 가리키는가?
  2. 분리된 모듈에서 <Routes> 내부에 <Route path=“/“> 가 필요한가?

해결 및 결론

  • App.js의 부모 Route에서 path에 와일드카드 * 사용하기
    • "뒤에 다른 요소가 붙어, 라우터로 이동한 페이지 내부에서 다시 다른 컴포넌트를 렌더링 할 것임" 을 알려줍니다.
  • 분리된 모듈에서 “/“ 경로는 어플리케이션 전체의 최상위 메인 페이지가 아니라, 해당 모듈에서의 최상위 페이지입니다.

코드로 확인하기

// 확인 코드 (Check.js)
function Event() {
	return (
		<>
			<h4>"/" 경로는 어디인가</h4>
			<Routes>
				<Route path="/" element={<div>현재 페이지를 가리키는가?</div>}/>
			</Routes>
		</>
	);
}
  • App.js 에서 이 컴포넌트를 임포트해서 사용 중이라고 한다면, 위의 <Route path="/" element={<div>현재 페이지를 가리키는가?</div>}/> 요소는 App.js 의 메인 페이지를 가리키는 것이 아니라 본인( Check.js )의 최상위 메인 페이지를 가리킵니다.
  • 따라서 분리된 모듈의 return 문 내부에 바로 엘리먼트를 작성하는 것과, <Route path="/"> 요소를 추가해서 그 안에 엘리먼트를 작성하는 것은 중복되므로 굳이 추가할 필요는 없는 것 같습니다.

💬 느낀점과 마무리

앞선 두 프로젝트보다 사용한 라이브러리도 많고, 작성한 코드의 양도 훨씬 늘어나면서 프론트엔드 개발 과정에 대한 이해도가 더 깊어질 수 있었습니다.

우선 현재 저의 학습 단계를 보다 명확하게 파악할 수 있었습니다. 직접 각 기술들을 사용하여 프로젝트를 진행해보니 계속해서 문제를 일으키는 개념이 어느 부분인지 알게 되었습니다. 그래서 프로젝트를 마치고 나서 해당 개념을 위주로 보완하여 공부하고, 더 성장할 수 있는 기회로 삼았습니다.

이에 맞물려 느낀 점은 프로젝트 과정에서 실제 구현 이외의 시간에 해당 기술과 개념을 별도로 공부하는 과정이 정말로 성장할 수 있는 기회라는 것이었습니다. 사실 라이브러리를 이것저것 갖다 붙이며 기능을 구현해보는 것은 정말 재미있지만, 이런저런 예제 코드를 참고하며 작성할 뿐 그 개념과 원리를 매번 하나하나 찾아가며 추가하기에는 시간이 너무 오래 걸렸습니다.

그래서 따로 개인 공부 시간을 확보하여, 그 시간만큼은 내가 작성했던 코드들을 되돌아보고 개념과 세부 원리를 복습하는 시간으로 사용했습니다. 앞서 라이브러리 없이 JS로 CRUD 구현하기 프로젝트에서 느꼈던 점과 같이, 이를 통해 기술의 간편함을 누리는 데서 그치지 않고 근본 개념을 충분히 이해하며 응용할 수 있을만큼 능숙해지고자 노력할 수 있었습니다.

profile
하루가 모여 역사가 된다

0개의 댓글