[React/Node.js] Bittersweet Web Page

Janet·2022년 12월 27일
0

Side Project

목록 보기
5/7
post-thumbnail

Project

Bittersweet Web Page

🔷 설명

  • ‘Bittersweet Korea’라는 임의의 커피 브랜드를 만들어 해당 브랜드 관련 내용을 소개 및 안내하는 웹 페이지를 구현한다.
  • 웹 페이지에는 신 메뉴를 홍보하는 메인 페이지를 비롯하여 브랜드 소개, 판매하는 제품 메뉴 안내, 자주 묻는 질문 정리, 공지사항 안내 게시판 및 해당 브랜드의 오프라인 매장 위치 찾기를 위한 지도 API 등을 포함한다.
  • 클라이언트는 CRA(Create React App)를 통해, 서버는 Node.js로 환경을 구축하기 위해 Express 프레임워크를 사용하였다. 로그인 및 게시판 CRUD 기능의 구현을 위해 Database는 MongoDB와 Mongoose를 사용하였다.
    • Mongoose는 Node.js와 MongoDB를 연결해주는 ODM(Object Document Mapping) 라이브러리이다.

🔷 폴더 구조 Tree

react-bittersweet
├── client
│   ├── build
│   ├── jsconfig.json
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   └── src
│       ├── App.js
│       ├── _actions
│       │   ├── types.js
│       │   └── user_action.js
│       ├── _reducers
│       │   ├── index.js
│       │   └── user_reducer.js
│       ├── components
│       │   ├── Footer.js
│       │   ├── Nav.js
│       │   ├── ScrollBtn.js
│       │   ├── faq
│       │   │   └── FormOfFaq.js
│       │   ├── menu
│       │   │   ├── FormOfMenu.js
│       │   │   └── FormOfMenuDetail.js
│       │   ├── store
│       │   │   └── KakaoMap.js
│       │   └── whatsnew
│       │       ├── FormOfNotice.js
│       │       ├── FormOfNoticeDetail.js
│       │       └── FormOfNoticeWrite.js
│       ├── css
│       │   └── App.module.css
│       ├── data
│       │   └── menuData.json
│       ├── hoc
│       │   └── auth.js
│       ├── hooks
│       │   └── useScrollFadeIn.js
│       ├── images
│       ├── index.js
│       ├── routes
│       │   ├── LoginPage.js
│       │   ├── RegisterPage.js
│       │   ├── aboutUs
│       │   │   ├── AboutUs.js
│       │   │   ├── BrandPrinciple.js
│       │   │   ├── Coffee.js
│       │   │   └── History.js
│       │   ├── faq
│       │   │   └── Faq.js
│       │   ├── home
│       │   │   └── Home.js
│       │   ├── menu
│       │   │   ├── Menu.js
│       │   │   ├── MenuBeverage.js
│       │   │   ├── MenuCoffee.js
│       │   │   ├── MenuDetail.js
│       │   │   └── MenuTea.js
│       │   ├── store
│       │   │   └── Store.js
│       │   └── whatsNew
│       │       ├── Notice.js
│       │       ├── NoticeDetail.js
│       │       ├── NoticeEdit.js
│       │       └── NoticeWrite.js
│       └── setupProxy.js
├── node_modules
├── package-lock.json
├── package.json
├── .gitignore
└── server
    ├── config
    │   ├── dev.js
    │   ├── key.js
    │   └── prod.js
    ├── index.js
    ├── middleware
    │   └── auth.js
    └── models
        ├── Posting.js
        └── User.js

🔷 추가할 기능들

  • FAQ 자주하는 질문 검색 기능 삽입
  • 메뉴 세부적으로 분류
    • MENU - 전체보기(All), 커피(Coffee), 티(Tea), 음료(Beverage)
  • 메뉴 디테일 페이지 상단에 페이지 링크 만들기 ex. Home > Menu > Category > Current Page
    • 해당 카테고리 페이지로 이동하려면 카테고리 마다 route를 새로 짜야 함.
  • 공지사항 게시판 구현하기
    • 게시판 CRUD 기능 구현
    • 글 게시하면 작성된 글번호 증가 기능: mongoose-sequence 라이브러리 통해 자동 증가 기능 사용하여 구현 id값 부여 및 증가
  • Find a Store 메뉴에 MAP API 넣기 - KAKAO MAP API
  • 제작 후 퍼블리싱까지하여 홈페이지 도메인 등록하기
    • 자세히 #1. 기존 방식
      • Freenom에서 무료 도메인 생성 후 AWS와 연동

      • 클라이언트는 Build파일 AWS S3에 배포하고 CloudFront와 Route 53 이용 및 ACM(AWS Certificate Manager)을 통해 SSL인증서 발급받고 HTTPS를 적용.

      • 서버는 AWS EC2를 통해 배포하고 SSL인증서 발급 및 AWS 로드밸런서 이용하여 HTTPS 적용
        - https://server.bittersweet.tk (서버 연결)

        #2.수정된 방식✅

      • 클라이언트와 서버 모두 EC2를 통해 배포 및 도메인 통일

      • SSL 인증서 발급은 AWS 로드밸런서가 아닌 Certbot, Nginx 및 Let’s Encrypt를 통해 진행

🔷 추가하면 좋을 기능들

  • 로그인, 회원가입, 게시판 CRUD 기능 구현하기
    • Node.js 서버 구현
    • MongoDB로 데이터베이스 관리

🔷 발생한 에러들

  • 개발 과정 발생 에러
    • 메뉴 카테고리 버튼 클릭 시, 해당되는 메뉴들 나열하기 기능을 구현하려 함. filter와 map함수를 이용하여 구현
      • ALL 카테고리(모든 메뉴 렌더링)만 안 되고 나머지 카테고리들은 정상적으로 나열되는 현상
        • includes()함수와 삼항연산자 사용하여 ALL버튼 눌렀을때(참) 모든 메뉴 렌더링, 나머지 카테고리 버튼 누르면 버튼의 value대로 메뉴 분류(거짓)
    • 메뉴 카테고리 별로 각각의 이미지를 Local JSON 파일에서의 로컬 경로 값으로 불러오고자 했으나, 로컬 이미지 경로가 require()로 안 불러와지는 문제 발생
      • JSON 파일에 입력한 url (절대 경로)
        "url": ”images/menu-coffee.jpg”
      • 로컬 주소 그대로 입력 시에는 작동.
        img src={require(”images/menu-coffee.jpg”)}
      • ❌ 아래 두 가지는 안 됨.
        img src={require(`${menu.url}`)}
        img src={require(`${menu.url}`).default}
      • ✅ 해결!
        // Menu.js
        
        <img src={require(`images/${*menu*.url}`)} />
        // menuData.json 수정
        
        "url": "menu-coffee.jpg”
        • 적용한 코드 - Menu.js 및 JSON 파일 일부 코드
          // menuData.json
          
          {
          	"data": [
              {
                "idx": "1",
                "name_ko": "아메리카노",
                "name_en": "Americano",
                "desc": "메뉴설명입니다.",
                "category": ["ALL", "COFFEE"],
                "temperature": "HOT",
                "열량": "10",
                "나트륨": "5",
                "포화지방": "0",
                "단백질": "1",
                "당류": "0",
                "카페인": "150",
                "url": "menu-coffee.jpg"
              },
          	]
          }
          // Menu.js
          
          <ul>
          	{filterCategory &&
          		filterCategory.map((menu) => {
          			return (
          				<div className={styles.menu_div} key={menu.idx}>
          				  <li>
          				    <Link to={`/menu-detail/${menu.idx}`}>
          				      <img src={require(`images/${menu.url}`)} alt={menu.url} />
          				    </Link>
          				  </li>
          				  <span>{menu.name_ko}</span>
          				</div>
          			)
          	})}
          </ul>
          
    • 유저가 페이지에 처음 접속 시 Login 버튼 보이게 하고, 로그인 성공하면 Logout 버튼이 보이게 하기.
      • 서버에서 user name을 보낼 코드를 작성하고, 클라이언트 Nav 컴포넌트에 axios로 유저 이름 받아올 코드 작성. useState(false)를 통해 로그인 안 한 경우 로그인 버튼 렌더링하고, 로그인 버튼 누르면 로그인 페이지로 이동한다. 사용자가 로그인에 성공하면 Home으로 이동되고 로그아웃 버튼 화면에 출력.
      • ❓ 로그인을 했는지 안 했는지 서버에서 받아온 유저 네임 데이터로 구분하기로 함. 로그인한 유저의 username을 로그아웃 버튼 옆에 출력하려고 했으나 로그인페이지에서 유저가 로그인을 성공하고 메인 홈페이지로 navigate된 이후에 자동으로 출력되지 않음. vs code에서 저장 후 새로고침하면 정상적으로 렌더링 됨. 이유가 뭔지?? 그냥 버튼에 <button>🔓LOG-OUT</button> 인 경우에는 정상적으로 출력.
        • useEffect는 컴포넌트가 렌더링된 직후에 실행되는 hook이라서 그런 것이 아닐까 싶은??
        • 콘솔창 에러 메세지
          • GET http://localhost:3000/생략 504 (Gateway Timeout)

          • Uncaught (in promise) AxiosError {message: 'Request failed with status code 504', name: 'AxiosError', code: 'ERR_BAD_RESPONSE', config: {…}, request: XMLHttpRequest, …}

            // server/index.js
            // 서버 측에서 작성한 UserName 보내기위한 코드
            
            // =====Get User Name=====
            app.get("/api/users/username", auth, (req, res) => {
              User.findById({ _id: req.user._id }, (err, user) => {
                if (err) return res.json({ success: false, err });
                // if (err) {
                //   return res.send("로그인");
                // }
                return res.status(200).send(req.user.name);
              });
            });
            // client/Nav.js
            // 클라이언트의 Nav Bar의 로그인/로그아웃 버튼 구현
            
            function Nav() {
              const navigate = useNavigate();
              const [isLogin, setIsLogin] = useState(false);
              const [logInUserName, setLogInUserName] = useState("");
            
              const onClickHandler = () => {
                axios.get("/api/users/logout").then((response) => {
                  if (response.data.success) {
                    navigate("/");
                    alert("로그아웃 하였습니다.");
                    setIsLogin(true);
                  } else {
                    alert("로그인 페이지로 이동합니다.");
                  }
                });
              };
            
              const getUserName = () => {
                axios.get("/api/users/username").then((response) => {
                  let user = response.data;
                  console.log(`유저네임: ${user}`);
                  if (user.toString().includes("object")) {
                    setIsLogin(true);
                    setLogInUserName("🔐LOG-IN");
                  } else if (user) {
                    setIsLogin(false);
                    // setLogInUserName(`${user}님 🔓LOG-OUT`);
                  }
                });
              };
            
              useEffect(() => {
                getUserName();
              }, [isLogin, logInUserName]);
            
              return (
                <div>
            			{/* ...생략 */}
                  {isLogin ? (
                    <Link to={`/login`}>
                      <button onClick={onClickHandler}>
                        {logInUserName}
                      </button>
                    </Link>
                  ) : (
                    <button onClick={onClickHandler}>
                      🔓LOG-OUT
                    </button>
                  )}
            			{/* ...생략 */}
            		</div>
    • 반응형 웹으로 만들었는데 실제 모바일 기기마다 출력 화면크기가 다르다거나 하는 등의 문제로 UI 위치들이 제각각 다른 문제가 생겼다.
      • ✅ 아래 2가지를 이용하여 실제 모바일 화면을 보며 CSS를 수정했다.
        • Toggle Device Toolbar: 크롬 콘솔창에서 이 기능을 통해 기기마다 화면 출력을 볼 수 있다. 하지만 실제 기기 화면과 약간의 차이가 있었다.
        • Xcode: IOS 개발 툴로 실제 기기와 정확히 일치하는 화면을 보여주었다.
        • 아래 스크린샷에서 왼쪽 화면이 Toggle Device Toolbar를 이용한 것이고, 오른쪽이 Xcode를 통해 출력한 것이다.
  • 배포 과정 발생 에러
    • 개발 환경에서의 로컬호스트와 배포 환경에서의 호스트 적용관련 이슈
      • client 폴더에 .env.development 파일과 .env.production 파일을 package.json 파일과 같은 경로에 생성하여 아래와 같이 환경변수 설정.
        - .env.development는 개발 로컬호스트로 그대로 적용하기 위해 공란으로 남김. 이렇게 되면 기존에 http-proxy-middleware 라이브러리를 통해 설정해둔 localhost:5000(개발 환경에서의 서버단의 포트번호)으로 적용되어 연결된다.
        - .env.production은 배포 후 서버 url을 입력하고, 클라이언트에서 axios를 통해 api를 호출 시, 해당 환경변수를 앞에 추가해줘야 했다.
        - 환경변수 선언 시 지켜야할 규칙: 꼭 REACT_APP 으로 시작되어야 함.

        //client 폴더 > .env.delvopment
        
        # process.env.REACT_APP_HOST
        REACT_APP_HOST=""
        //client 폴더 > .env.production
        
        # process.env.REACT_APP_HOST
        REACT_APP_HOST=https://server.bittersweet.tk
        //client에서 환경변수 추가 
        
        	const getPosts = () => {
            axios.get(`${process.env.REACT_APP_HOST}/api/posting`).then((response) => {
              let post = response.data;
              setPosts(post);
            });
          };
    • DB 에러 (mongoDB)
      • server 폴더 > config 폴더 >
        • key.js (환경변수 조건문 설정)

          if (process.env.NODE_ENV === "production") {
            module.exports = require("./prod");
          } else {
            module.exports = require("./dev");
          }
        • dev.js (개발 환경): mongoDB localhost 27017 포트 번호로 로컬에 생성한 DB로 연결.

          module.exports = {
            mongoURI: "mongodb://localhost:27017/생성한DB명",
          };
      • prod.js (배포 후 환경)
        module.exports = {
          mongoURI: process.env.MONGO_URI,
        };
        • 배포 후 AWS EC2의 ubuntu에서 mongoDB 설치 후 환경변수를 설정해주었다.
        • NODE_ENV를 production으로 설정해주고 그에 따라 MONGO_URI 값도 설정해줌.
          • 환경변수 영구 설정 하기 - 리눅스에서 환경 변수 설정은 아래와 같이 하면 된다. i를 눌러 insert mode 진입하여 입력 후 esc눌러서 입력모드 해제 후 , :wq 입력하여 빠져나온다.
            vi ~/.bashrc
          • bashrc 파일 맨 밑에 환경변수 추가
            export NODE_ENV="production"
            export MONGO_URI="mongodb+..."
          • 이후 환경변수 적용하려면 컴퓨터 재시작 혹은 다음 코드 입력해준다.
            • source ~/.bashrc
    • CORS 에러
      • Access to XMLHttpRequest at '[https://server.bittersweet.tk/api/users](https://server.bittersweet.tk/api/users)' from origin '[https://www.bittersweet.tk](https://www.bittersweet.tk/)' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
      • 해결: 서버 단에서 cors 라이브러리 설치하여 클라이언트 url과의 접속을 허용하도록 설정해줌.
        • npm i cors

          //server 폴더 > index.js
          
          const cors = require("cors");
          
          //CORS ISSUE
          const clientURL = ["https://bittersweet.tk", "https://www.bittersweet.tk"];
          let corsOptions = {
            origin: function (origin, callback) {
              if (clientURL.indexOf(origin) !== -1) {
                //URL배열에 origin 인자가 있을 경우
                callback(null, true); //cors 허용
              } else {
                callback(new Error("Not allowed by CORS")); //cors 비허용
              }
            },
            credentials: true,
          };
          app.use(cors(corsOptions));
      • 추가적인 문제 발생: 위와 같은 방법을 통해 bittersweet.tkwww.bittersweet.tk로 접속은 가능했으나, 도메인 접속이 정상적으로 되다가 종종 접속 권한없음 에러가 뜨며 불안정한 접속 문제가 발생했다. ✅ 이후 서버와 클라이언트가 통신하는 도메인을 통일하였고, S3에 따로 업로드했던 클라이언트를 서버와 함께 AWS EC2로 옮겨주었다. HTTPS 사용을 위해 SSL인증서를 Let’s Encrypt를 통해 인증서를 발급받았다. cors 이슈 관련 코드는 다음과 같이 수정하였다.
        //server 폴더 > 수정된 index.js
        
        const cors = require("cors");
        
        let corsOptions = {
          origin: [
            "http://localhost:3000",
            "https://bittersweet.ml",
            "https://www.bittersweet.ml",
          ],
          credentials: true,
        };
        app.use(cors(corsOptions));
    • 배포 후 유저 로그인 시 메인 화면으로 이동 후 로그인이 자동으로 풀려버리는 현상 발생
      • 배포 후 로그인 기능은 작동하나, auth 체크 기능 미작동? 로그인은 되어도 네비게이션 헤더 부분에 로그인상태, 로그아웃 상태가 반영되지 않았다. (예: 로그인 시 로그아웃 버튼 보여지기) http, https 환경에서 보안때문에 쿠키 기능이 작동되기 어렵다는 것을 알게됐음.
      • 참고링크 : https://velog.io/@code-bebop/CORS의-Cookie
      • 참고링크: https://grownfresh.tistory.com/163
      • 해결: withCredentials: true쿠키를 받을 요청 뿐만 아니라 쿠키를 보낼 요청 또한 withCredentials 옵션을 true로 설정해줬다.
        //client > user.action.js
        
        export function loginUser(dataToSubmit) {
          //dataToSubmit은 LoginPage의 body(이메일, 패스워드)를 parameter로 받는 것임.
          const request = axios
            .post(`${process.env.REACT_APP_HOST}/api/users/login`, dataToSubmit, {
              withCredentials: true,
            })
            .then((response) => response.data);
        
          return {
            type: LOGIN_USER,
            payload: request,
          };
        }
        
        export function auth() {
          const request = axios
            .get(`${process.env.REACT_APP_HOST}/api/users/auth`, {
              withCredentials: true,
            })
            .then((response) => response.data);
        
          return {
            type: AUTH_USER,
            payload: request,
          };
        }
        //client > Nav.js
        
        	const onClickHandler = () => {
            axios
              .get(`${process.env.REACT_APP_HOST}/api/users/logout`, {
                withCredentials: true,
              })
              .then((response) => {
                if (response.data.success) {
                  navigate("/");
                  alert("로그아웃 하였습니다.");
                  setIsLogin(true);
                }
              });
          };
        
          const getUserName = () => {
            axios
              .get(`${process.env.REACT_APP_HOST}/api/users`, {
                withCredentials: true,
              })
              .then((response) => {
                let user = response.data;
                console.log(user);
                if (user.toString().includes("object")) {
                  setIsLogin(true);
                  setLogInUserName("🔐LOG-IN");
                } else {
                  setIsLogin(false);
                  // setLogInUserName(`${user}님 🔓LOG-OUT`);
                }
              });
          };
    • AWS S3 버킷에 업로드된 파일을 삭제 후 변경된 build 파일들을 새로 업데이트했으나 도메인 접속 시 변경 사항이 적용되지 않는 문제 발생. S3 도메인으로는 변경이 적용되었으나, CloudFront로 배포한 웹사이트의 도메인에는 업데이트되지 않았음.
      • 문제 원인: CloudFront로 배포되는 파일의 캐시 유지시간은 기본 24시간이며, Origin HTTP Header의 캐시 설정(Cache-Control)을 이용해 캐시가 유지되는 시간을 자유롭게 설정 할 수 있습니다. 즉, S3 버킷의 내용을 변경했다고 하더라도 캐시가 유지되는 시간이내에서는 해당 변경내용이 CloudFront에 반영되지 않습니다. 만약 기본 유지시간을 사용한다면, 24시간이 지나야 해당 변경내용이 반영됩니다.
      • 해결: 캐시정책 시간 이전에 강제로 CloudFront의 배포내용을 업데이트하고 싶다면? CloudFront의 배포 설정에 들어가서 Invalidation(무효화) 기능을 사용한다.
    • OG(Open Graph) 변경 후 미적용 문제
    • EC2의 Ubuntu를 통해 배포했던 서버와 서버의 SSL 인증을 위해 AWS Load Balancer 사용 중 유료 대금 발생하였다.
      • 해결:
        • AWS Load Balancer Free tier 초과로 SSL인증을 위한 다른 루트를 검색했다.
        • 로드밸런서 대신에 Certbot, Nginx, Let’s Encrypt를 통해 SSL무료 인증을 받을 수 있다는 것을 알게되었고 SSL 인증을 새롭게 받았다.
      • 추가적인 문제:
        • S3의 클라이언트와 EC2의 서버 통신이 정상적으로 이루어지지 않아서, 클라이언트를 EC2를 통해 함께 배포하기로 했다.
      • 추가 문제 해결: 도메인 또한 서버 배포용 도메인을 따로 만들지 않고, 하나의 도메인으로 통일하여 새롭게 SSL 인증을 받았다.

profile
😸

0개의 댓글