관리 메뉴

거니의 velog

(1) 프론트엔드 프로젝트 : 시작 및 회원 인증 구현 1 본문

React_프론트엔드 프로젝트

(1) 프론트엔드 프로젝트 : 시작 및 회원 인증 구현 1

Unlimited00 2024. 2. 21. 17:50

* 이 장에서는 리액트 프로젝트를 생성하여 지금까지 만든 서버에 연동해 볼 것이다. 프로젝트를 만들어 가면서 지금까지 배운 다양한 기술을 활용해 줄 것이다. 이 과정을 통해 실무에서 프로젝트를 개발할 때 어떤 방식으로 작업하는지 알 수 있다.

* 프로젝트를 시작하기 전에 앞으로 어떤 기능을 개발하는 지 간략하게 살펴보자.

(1) 회원가입/로그인 기능

(2) 글쓰기 기능 : 이 과정에서 Quill 이라는 WYSIWYG 에디터 라이브러리를 사용한다.

* WYSIWYG 란 'What You See Is What You Get'의 약어이다.
  사용자가 보는 대로 결과를 얻는다는 의미이며, '위지윅'이라고 읽는다.
  위지윅 에디터에서는 글을 쓸 때 HTML을 직접 입력하면서 스타일을 설정하는 것이 아니라,
  에디터에서 지원되는 기능을 사용하여 간편하게 스타일을 설정할 수 있다.

(3) 블로그 포스트 목록 보여주는 기능 / 포스트를 읽는 기능 구현

(4) 포스트를 수정하거나 삭제할 수 있는 기능까지 구현

* 이제 본격적으로 개발을 시작해 보자. 이 프로젝트는 총 네 번에 걸쳐서 진행되며, 이번 장의 실습은 다음 흐름으로 진행된다.


1. 작업 환경 준비하기

* 새 리액트 프로젝트를 생성해 보자. 리액트 프로젝트는 기존에 blog-backend 디렉터리가 있는 blog 디렉터리에 생성하자.

$ yarn create react-app blog-frontend

* 버전 충돌을 피하기 위해 package.json은 다음과 같이 세팅 후 재설치한다.

{
  "name": "blog-frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "immer": "^3.1.3",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-redux": "^7.1.0",
    "react-router-dom": "^5.0.1",
    "react-scripts": "3.0.1",
    "redux": "^4.0.1",
    "redux-actions": "^2.6.5",
    "redux-devtools-extension": "^2.13.8",
    "styled-components": "^4.3.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

* 프로젝트를 VS Code로 열 때 새로운 창에서 열어도 되지만, 기존 blog-backend 디렉터리가 열려 있는 창에서 작업 영역에 폴더 추가... 기능으로 열면 VS Code 창 하나에서 두 프로젝트를 관리할 수 있어 좀 더 편하게 작업할 수 있다.

* 이렇게 추가하면 VS Code 에서 두 프로젝트를 한꺼번에 관리할 수 있다.

* 작업 영역을 만들고 나서 나중에도 이렇게 열고 싶다면 VS Code 메뉴의 파일 > 작업 영역을 다른 이름으로 저장 을 클릭하자. 작업 영역 파일을 blog 디렉터리에 blog라는 이름으로 저장하면 된다(이름은 원하는 대로 설정할 수 있다).

* 그냥 blog 디렉터리를 열었을 때보다 작업 영역을 통해 두 프로젝트를 열었을 때 자동 import 기능이 더욱 완벽하게 작동한다.


(1) 설정 파일 만들기

* 우리가 만든 프로젝트에 필요한 설정 파일을 만들어 보자. 먼저 프로젝트의 코드 스타일을 정리해 주는 Prettier의 설정 파일을 만들자. 여러분의 취향에 따라 옵션들을 커스터마이징해도 상관없다.

* 다음 파일을 blog-frontend 디렉터리에 생성하자.

{
    "singleQuote": true,
    "semi": true,
    "useTabs": false,
    "tabWidth": 2,
    "trailingComma": "all",
    "printWidth": 80
}

* 그리고 프로젝트에서 자동 import 기능이 제대로 작동할 수 있게 jsconfig.json 파일을 생성하자.

{
    "compilerOptions": {
        "target": "ES6"
    }
}

(2) 라우터 적용

* 프로젝트를 처음 만들고 나서 설계를 시작할 때 가장 먼저 무엇을 하면 좋을까? 바로 리액트 라우터를 프로젝트에 설치하고 적용하는 것이다. 앞으로 만들게 될 주요 페이지의 라우트 컴포넌트를 미리 만들자. 먼저 틀을 갖춰 놓고 하나하나 개발하면 편하다.

* 우선 react-router-dom 라이브러리를 설치하자.

$ yarn add react-router-dom

* 다음으로 총 다섯 개의 페이지를 만든다. 라우트와 관련된 컴포넌트들은 src/pages 디렉터리에 만들겠다.

* 앞으로 만들 라우트 컴포넌트는 다음과 같다.

(1) LoginPage.js - 로그인

(2) RegisterPage.js - 회원가입

(3) WritePage.js - 글쓰기

(4) PostPage.js - 포스트 읽기

(5) PostListPage.js - 포스트 목록

* 자, 이제 이 컴포넌트를 하나씩 만들어 주자.

[src/pages/LoginPage.js]

import React from 'react';

const LoginPage = () => {
  return <div>로그인</div>;
};

export default LoginPage;

[src/pages/RegisterPage.js]

import React from 'react';

const RegisterPage = () => {
  return <div>회원가입</div>;
};

export default RegisterPage;

[src/pages/WritePage.js]

import React from 'react';

const WritePage = () => {
  return <div>글쓰기</div>;
};

export default WritePage;

[src/pages/PostPage.js]

import React from 'react';

const PostPage = () => {
  return <div>포스트 읽기</div>;
};

export default PostPage;

[src/pages/PostListPage.js]

import React from 'react';

const PostListPage = () => {
  return <div>포스트 리스트</div>;
};

export default PostListPage;

* 라우트 컴포넌트 다섯 개를 모두 생성했는가? 이제 프로젝트 엔트리 파일인 index.js에서 BrowserRouter로 App 컴포넌트를 감싸자.

[src/index.js]

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root'),
);

* 다음으로는 App 컴포넌트에서 Route 컴포넌트를 사용하여 각 라우트의 경로를 지정해 주자.

[src/App.js]

import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import WritePage from './pages/WritePage';
import PostPage from './pages/PostPage';

function App() {
  return (
    <>
      <Route component={PostListPage} path={['/@:username', '/']} exact />
      <Route component={LoginPage} path="/login" />
      <Route component={RegisterPage} path="/register" />
      <Route component={WritePage} path="/write" />
      <Route component={PostPage} path="/@:username/:postId" />
    </>
  );
}

export default App;

* 위 라우트 중 PostListPage를 보면 Path에 배열을 넣어 주었다. 이렇게 배열을 넣어 주면 한 라우트 컴포넌트에 여러 개의 경로를 쉽게 설정할 수 있다.

* 이렇게 배열을 사용하지 않는다면 다음과 같이 작성할 수도 있다.

<Route component={PostListPage} path="/" exact />
<Route component={PostListPage} path="/@:username" exact />

* Route 컴포넌트를 두 번 사용하는 것보다는 배열을 넣는 것이 훨씬 편하다. 이러한 기능은 리액트 라우터 v5부터 사용할 수 있다.

* 추가로 path에 '/@username' 이라고 입력한 것이 조금 생소할 수 있는데, 이 경로는 http://localhost:3000/@bbbb7788 같은 경로에서 bbbb7788을 username의 파라미터로 읽을 수 있게 해 준다. Medium, 브런치 같은 서비스에서도 계정명을 주소 경로 안에 넣을 때 주소 경로에 @ 을 넣는 방식을 사용한다.

* Route 지정을 모두 마쳤다면 blog-frontend 경로에서 yarn start 명령어를 입력한다. 그리고 다음 경로에 들어가서 알맞은 컴포넌트가 뜨는지 확인해 보자.

- http://localhost:3000/

- http://localhost:3000/@tester

- http://localhost:3000/write

- http://localhost:3000/@tester/1234

- http://localhost:3000/login

- http://localhost:3000/register

* 비어 있는 페이지 없이 이전에 라우트 컴포넌트 안에 넣어 준 '포스트 리스트', '회원가입' 등의 문구가 뜬다면 잘 작동하는 것이다.


(3) 스타일 설정

* 이번 프로젝트에서는 styled-components를 사용하여 스타일링할 것이다. 여러분이 styled-components 가 아닌 다른 방식으로 스타일링 하는 것을 좋아한다면, 여기서 사용하는 스타일을 참고하여 Sass, CSS Module 등의 방법으로 작성해도 좋다.

* 먼저 styled-components를 설치해 주자.

$ yarn add styled-components

* 다음으로 나중에 색상을 사용할 때 쉽게 뽑아서 사용할 수 있도록 색상 팔레트 파일을 만든다.

* src/lib/styles 디렉터리를 만들고, 그 안에 palette.js 라는 파일을 다음과 같이 작성하자. 색상 HEX 코드를 하나하나 입력하기가 번거로울 수 있으니 https://bit.ly/mypalette 경로에 들어가서 코드를 복사한 후 사용하자.

https://gist.github.com/velopert/fda9083ddedcbbef30bb40362f9d01c4

 

palette.js

palette.js. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

// source: https://yeun.github.io/open-color/

const palette = {
  gray: [
    '#f8f9fa',
    '#f1f3f5',
    '#e9ecef',
    '#dee2e6',
    '#ced4da',
    '#adb5bd',
    '#868e96',
    '#495057',
    '#343a40',
    '#212529',
  ],
  cyan: [
    '#e3fafc',
    '#c5f6fa',
    '#99e9f2',
    '#66d9e8',
    '#3bc9db',
    '#22b8cf',
    '#15aabf',
    '#1098ad',
    '#0c8599',
    '#0b7285',
  ],
};

export default palette;

* 위 색상은 open-color 라는 라이브러리에서 추출했다. 이 라이브러리를 yarn으로 설치하여 사용할 수도 있다. 하지만 이렇게 따로 palette를 만들어 사용하면 open-color에서 제공하는 모든 색상을 불러와 사용하는 것이 아니라 필요한 색상만 불러와서 사용할 수 있고, 자동 import가 좀 더 제대로 작동하기 때문에 더욱 편하다(open-color 라이브러리를 설치해서 사용하면 open-color 안의 색상들이 자동 import 되지 않는다).


(4) Button 컴포넌트 만들기

* 먼저 검정색 버튼을 스타일링해 보자. 이 버튼 컴포넌트는 다양한 곳에서 재사용할 예정이다. 그러므로 src 디렉터리에 components/common 디렉터리를 생성하고 그 안에 이 컴포넌트를 만들어 줄 것이다.

import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';

const StyledButton = styled.button`
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  font-weight: bold;
  padding: 0.25rem 1rem;
  color: white;
  outline: none;
  cursor: pointer;

  background: ${palette.gray[8]};
  &:hover {
    background: ${palette.gray[6]};
  }
`;

const Button = (props) => <StyledButton {...props} />;

export default Button;

* 사실 이 컴포넌트에서 StyledButton 을 바로 내보내도 상관없다. 하지만 굳이 Button 리액트 컴포넌트를 만들어서 그 안에 StyledButton 을 렌더링해 준 이유는 추후 이 컴포넌트를 사용할 때 자동 import 가 되게 하기 위해서이다. styled-components로 만든 컴포넌트를 바로 내보내면 자동 import가 제대로 작동하지 않는다.

* Button 컴포넌트를 만드는 과정에서 {...props}를 StyledButton에 설정해 주었는데, 이는 Button이 받아 오는 props를 모두 StyledButton에 전달한다는 의미이다.

* 컴포넌트를 다 만들었으면 이 컴포넌트를 PostListPage 컴포넌트에 렌더링해 보자.

[src/pages/PostListPage.js]

import React from 'react';
import Button from '../components/common/Button';

const PostListPage = () => {
  return (
    <div>
      <Button>버튼</Button>
    </div>
  );
};

export default PostListPage;

* 브라우저의 http://localhost:3000/ 경로에 방금 만든 버튼이 잘 나타나는가?

* 버튼이 잘 만들어졌다면 프로젝트의 글로벌 스타일을 수정한다. index.css를 열어서 다음과 같이 수정해 주자.

body {
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  box-sizing: border-box; /* 엘리먼트의 box-sizing 값을 border-box로 설정 */
  min-height: 100%;
}

#root {
  min-height: 100%;
}

/* 추후 회원인증 페이지에서
 배경화면을 페이지의 전체 영역에 채우기 위한 용도 */
html {
  height: 100%;
}

/* 링크에 색상 및 밑줄 없애기 */
a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: inherit; /* 모든 엘리먼트의 box-sizing 값을 border-box로 설정 */
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

(5) 리덕스 적용

* 이제 프로젝트에 리덕스를 적용해 보자. 추후 비동기 작업을 관리하는 과정에서 redux-saga를 쓸 텐데 지금 당장은 미들웨어에 대한 관심은 접어 둔 채 리덕스 스토어를 생성하고 Provider 컴포넌트를 통해 프로젝트에 리덕스를 적용하는 과정만 다룰 것이다.

* 리덕스 사용에 필요한 라이브러리를 설치해 주자.

$ yarn add redux react-redux redux-actions immer redux-devtools-extension

* 이번 프로젝트에서 리덕스를 사용하는 데 immer 라이브러리가 꼭 필요하지는 않다. 하지만 immer를 사용하여 불변성을 좀 더 편하게 관리하려고 한다. 만약 immer 없이, spread 연산자를 활용하여 불변성을 관리하는 것이 더 편하다면 굳이 사용하지 않아도 된다. 

* 라이브러리를 설치했으면 첫 번째 리덕스 모듈을 만들어 준다. 이 프로젝트에서는 Ducks 패턴을 사용하여 액션 타입, 액션 생성 함수, 리듀서가 하나의 파일에 다 정의되어 있는 리덕스 모듈을 작성할 것이다.

* src/modules 디렉터리를 만들고, 그 안에 auth.js 라는 모듈을 생성하자. 세부 기능은 추후 구현하겠다. 일단 리듀서의 틀만 만들어서 내보내 주자.

import { createAction, handleActions } from 'redux-actions';

const SAMPLE_ACTION = 'auth/SAMPLE_ACTION';

export const sampleAction = createAction(SAMPLE_ACTION);

const initialState = {};

const auth = handleActions(
  {
    [SAMPLE_ACTION]: (state, action) => state,
  },
  initialState,
);

export default auth;

* 다음으로 루트 리듀서를 만들자. 지금은 리듀서가 하나밖에 없지만, 나중에는 여러 가지 리듀서를 더 만들 것이다.

[src/modules/index.js]

import { combineReducers } from 'redux';
import auth from './auth';

const rootReducer = combineReducers({
  auth,
});

export default rootReducer;

* 루트 리듀서를 만든 후에는 프로젝트의 엔트리 파일 index.js에서 스토어를 생성하고, Provider를 통해 리액트 프로젝트에 리덕스를 적용하자.

[src/index.js]

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './modules/index';

const store = createStore(rootReducer, composeWithDevTools());

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById('root'),
);

* 리덕스를 적용한 뒤에는 크롬 개발자 도구의 Redux 탭을 열어서 auth 객체가 존재하는지 확인해 보자.

리덕스 적용 확인

* 이제 프로젝트 작업 환경에 대한 준비를 마쳤다. 본격적으로 기능을 구현해 보자!