인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

루룸님의 프로필 이미지
루룸

작성한 질문수

Slack 클론 코딩[실시간 채팅 with React]

배포 방법

작성

·

122

0

제가 백엔드 강의는 수강한 적이 없어서요, 대신 노드js 교과서 책을 구매해서 가지고 있는데..

우선 프론트는 네트리파이로 배포 완료했습니다
https://admirable-donut-f22cc6.netlify.app/

백엔드 배포는 선생님 책 노드js 교과서 722쪽 AWS 배포하기 부터 보면서 하면 별 문제없지 진행할 수 있을까요? 추가적으로 백엔드쪽 코드 수정이 필요할지..

배포할 레포 구조는 아래 처럼 루트 폴더 하위에 백엔드, 프론트 폴더 각각 있습니다

답변 2

0

저는 제 프로젝트를 배포하는 작업을 진행 중인데, Node.js 교재에서 제공하는 AWS를 이용한 백엔드 배포 방법을 따르고 있습니다. 설명은 직관적이지만 실제로 설정할 때 문제가 생길까봐 걱정이 됩니다. 프론트엔드는 이미 Netlify에 배포되어 있기 때문에 백엔드 코드가 큰 변경 없이 잘 작동할지 확인하고 싶습니다.

그런데 다른 호스팅 옵션을 찾아보다가 Vultr을 발견했어요. 백엔드 호스팅을 위해 정말 좋은 선택인 것 같습니다. 최적화된 VPS 호스팅, 자동 백업, 보안 데이터베이스 관리 등 유용한 기능이 많이 있어서 훨씬 더 쉬운 작업을 할 수 있게 되더군요.

Node.js 앱을 작업 중이거나 MySQL 데이터베이스에 신뢰할 수 있는 호스팅이 필요하다면 Vultr을 추천드립니다. 특히 Managed Databases 서비스는 데이터베이스 관리를 정말 쉽게 해주기 때문에 유용하게 사용할 수 있습니다.

Vultr에서 MySQL을 설치하는 데 유용한 가이드를 공유할게요:

저는 이제 백엔드 배포를 위해 Vultr로 전환할 생각을 하고 있는데, 이 서비스를 사용해보면 여러분에게도 정말 좋은 선택이 될 거라고 확신합니다!

0

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

백엔드쪽은 책 따라서 하시면 되는데요. cors는 전체 허용을 해두셔야 합니다. 프론트 백엔드 도메인이 서로 다를 것 같아서요. 그래서 로그인 시 쿠키가 전달 안 되는 문제가 있을 수도 있습니다. 쿠키 대신 토큰 로그인을 하는 게 좋으나 책에는 그 부분은 없습니다.

루룸님의 프로필 이미지
루룸
질문자

AWS lightsail에서 mysql-server 설치하는데 에러가 나서요

책 내용 중

$ sudo apt-get update

$ sudo apt-get install -y gnupg

$ sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.23-1_all.deb

$ sudo dpkg -i mysql-apt-config_0.8.23-1_all.deb

$ sudo apt update

$ sudo apt-get install -y mysql-server -> 여기서 에러

에러 메세지 -> lightsail mysql server has no installation candidate

비번 설정하시고, Use Legacy Authentication Method 선택하세요.

$ sudo mysql -uroot -p

image.png

인스턴스를 지우고 다시 해봐도 똑같습니다

강좌에 백엔드 배포 영상도 있으면 좋을텐데 아쉽네요 ㅜ

 

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

라이트세일 우분투 고르셨나요? 우분투라면 아래 링크 방법으로 설치 가능합니다.

https://www.digitalocean.com/community/tutorials/how-to-install-mysql-on-ubuntu-20-04

 데비안같긴 한데 데비안이라면

https://www.digitalocean.com/community/tutorials/how-to-install-the-latest-mysql-on-debian-10

입니다.

루룸님의 프로필 이미지
루룸
질문자

image.pngimage.png

우분투나 데비안 선택지는 따로 없었고, 책에 나온대로 이렇게 선택했는데 이게 데비안인가요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

lsb_release -si 

입력할 때 나오는 이름이 운영체제입니다.

루룸님의 프로필 이미지
루룸
질문자

https://www.digitalocean.com/community/tutorials/how-to-install-the-latest-mysql-on-debian-10

데비안 기준으로 알려주신 위 링크에서 진행 중 아래 단계에서 에러가 나서요

image.png

에러 화면

image.png

한 5시간째 gpt, 구글링 다 해보고 있는데 mysql 설치가 안되네요 ㅜ

 

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

데비안 버전 문제로 보이는데요. 중간에 wget 쪽에 문자열을 아래처럼 최신 deb 파일로 수정한 뒤 진행하시면 될겁니다. (라이트세일에서 확인완료)

wget https://dev.mysql.com/get/mysql-apt-config_0.8.32-1_all.deb

루룸님의 프로필 이미지
루룸
질문자

최신 deb 파일로 하니 설치는 잘 되었는는데

이후 mysql 비밀번호 설정하고 git clone 받아서

아파치 서버 종료하고

클론 받은 폴더에서 패키지 설치 후 npm sequelize db:create --ene production 하

access denied for user 'root'@'localhost' 에러가 나서요

image.png

<백엔드 코드>

image.png

app.js

const express = require("express");
const dotenv = require("dotenv");
const morgan = require("morgan");
const session = require("express-session");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const path = require("path");
const hpp = require("hpp");
const helmet = require("helmet");
const passport = require("passport");

dotenv.config();
const { sequelize } = require("./models");
const passportConfig = require("./passport");
const apiRouter = require("./routes/api");
const webSocket = require("./socket");

const app = express();
app.set("PORT", process.env.PORT || 3095);
sequelize
  .sync()
  .then(() => {
    console.log("DB 연결 성공");
  })
  .catch(console.error);
passportConfig();
const prod = process.env.NODE_ENV === "production";

if (prod) {
  app.enable("trust proxy");
  app.use(morgan("combined"));
  app.use(helmet({ contentSecurityPolicy: false }));
  app.use(hpp());
} else {
  app.use(morgan("dev"));
  app.use(
    cors({
      origin: true,
      credentials: true,
      webSocket: true,
    })
  );
}
app.use(express.static(path.join(__dirname, "public")));
app.use("/uploads", express.static(path.join(__dirname, "uploads")));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser(process.env.COOKIE_SECRET));
const sessionOption = {
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
  },
};
if (prod) {
  sessionOption.cookie.secure = true;
  sessionOption.cookie.proxy = true;
}
app.use(session(sessionOption));
app.use(passport.initialize());
app.use(passport.session());

app.use("/api", apiRouter);
app.get("*", (req, res, next) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

const server = app.listen(app.get("PORT"), () => {
  console.log(`listening on port ${app.get("PORT")}`);
});

webSocket(server, app);

config.js

require('dotenv').config();

module.exports = {
  "development": {
    "username": "root",
    "password": process.env.SEQUELIZE_PASSWORD,
    "database": "sleact",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "test": {
    "username": "root",
    "password": process.env.SEQUELIZE_PASSWORD,
    "database": "sleact",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": process.env.SEQUELIZE_PASSWORD,
    "database": "sleact",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}

.env

COOKIE_SECRET=sleactcookie
MYSQL_PASSWORD=jsmaster
SEQUELIZE_PASSWORD=jsmaster

package.json

{
  "name": "sleact-ts-back",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "dev": "nodemon app",
    "start": "cross-env NODE_ENV=production PORT=80 pm2 start app.js"
  },
  "author": "ZeroCho",
  "license": "MIT",
  "dependencies": {
    "bcrypt": "^5.1.0",
    "cookie-parser": "^1.4.5",
    "cors": "^2.8.5",
    "cross-env": "^7.0.3",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "express-session": "^1.17.1",
    "helmet": "^4.4.1",
    "hpp": "^0.2.3",
    "morgan": "^1.10.0",
    "multer": "^1.4.2",
    "mysql2": "^2.2.5",
    "nodemon": "^2.0.7",
    "passport": "^0.6.0",
    "passport-local": "^1.0.0",
    "pm2": "^4.5.4",
    "sequelize": "^6.5.0",
    "sequelize-cli": "^6.2.0",
    "socket.io": "^4.7.5"
  }
}

 

 

 

 

 

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

.env 파일은 서버에도 만드신 게 맞나요? gitignore에 추가되어있는 파일은 git clone시 생성 안 됩니다

루룸님의 프로필 이미지
루룸
질문자

앗 .env도 ignore에 포함됐었군요 ㅜ .env도 추가하니 잘 실행 완료되었는데

회원가입에서 500 에러가 나고 있습니다 ㅜ

app.js:2 POST http://43.201.1.136/api/users 500 (Internal Server Error)

 

image.png

<회원가입 클릭 시 코드>

const onSubmit = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      if (!missmatchError && nickname) {
        console.log('회원가입 시도');
        setSignUpError('');
        setSignUpSuccess(false);
        // localhost 3090(프론트)가 3095(백엔드)로 보내는 요청
        axios
          .post('/api/users', {
            email,
            nickname,
            password,
          })
          .then(() => {
            setSignUpSuccess(true);
          })
          .catch((error) => {
            console.log(error.response);
            setSignUpError(error.response.data);
          })
          .finally(() => {});
      }
    },
    [email, nickname, password, passwordCheck, missmatchError],
  );

http://43.201.1.136/api/users 주소 접속해보면 정상적으로 보이는데.. 어디서 잘못되었는지 모르겠습니다

500 에러(Internal Server Error)의 경우에는 사용자가 아닌 서버 자체의 오류로 인해서 사이트의 모든 페이지에 접근이 불가능한 상태가 발생이라고 하는

서버쪽 코드를 수정해야할까요?

 

 

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

브라우저에 /api/users 치는 건 GET이고요. 지금 에러가 나는건 POST입니다. 서버에서 에러 로그 확인해서(pm2 monit) 에러 메시지 따라서 수정해야 합니다.

혹시 sequelize db:create 같은 명령어 수행하셨나요? 데이터베이스, 테이블이 생성되어 있어야 합니다.

루룸님의 프로필 이미지
루룸
질문자

넵 데이터는 npx sequelize db:create --env production 명령어로 생성했습니다

image.png

pm2 monit 실행 했을 때는 아무 내용이 안 나오더라구요

image.png

 

이상한게 회원가입 1번째 클릭엔 500에러인데 2번쨰 클릭에는 이미 사용 중인 아이디라고 403 에러가 뜨고 있습니다

회원가입 정보 db는 추가되고 있는 것 같아요

image.png

회원가입 실패한 정보로 로그인 시도를 해보면 200 코드로 나오고 있습니다

image.png

db를 초기화해야 할까요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

sudo 로 Pm2 실행했으면 sudo pm2 monit 하셔야 합니다. 다시 에러 조회해보세요.

루룸님의 프로필 이미지
루룸
질문자

image.png

cannot read properties of null(reading 'addMembers') 라고 나오고 있습니다

해당 코드 부분

image.png

강의 초반에 아래 단계를 진행했었는데

npm i npx sequelize db:create // mysql db에 sleact 데이터(로우) 생성
npx sequelize db:seed:all // 워크스페이스 > 채널에 가짜 데이터 생성
npm run dev // 테이블 생성

 

여기서도 똑같이 npx sequelize db:seed:all 이것도 해줘야 하는건가요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

네네 해주셔야 합니다.

루룸님의 프로필 이미지
루룸
질문자

아 이제 회원가입은 되는데 로그인이 안 되네요... ㅋㅋㅋ

image.png

login response 값으로 {"id":11,"nickname":"qwer","email":"qwer"} 잘 나오는데

user response 값은 false로 나오고 있습니다

image.png

<Login.tsx 코드>

const Login = () => {
  // useSWR은 get으로 요청한 데이터를 받아와서 저장한다.
  // mutate : 내가 원할 때 SWR 호출하기
  const { data, error, mutate } = useSWR('/api/users', fetcher, {
    dedupingInterval: 5000, // 주기적으로 호출하지만, dedupingInterval 기간 내에는 캐시에서 불러온다
  });
  const [logInError, setLogInError] = useState(false);
  const [email, setEmail, onChangeEmail] = useInput('');
  const [password, setPassword] = useInput<string>('');

  const onChangePassword = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setPassword(e.target.value);
    },
    [email, password, data],
  );

  const onSubmit = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      setLogInError(false);
      axios
        .post(
          '/api/users/login',
          { email, password },
          {
            withCredentials: true,
          },
        )
        .then(() => {
          mutate();
        })
        .catch((error) => {
          setLogInError(error.response?.status === 401);
        });
    },
    [email, password],
  );

  if (data) return <Navigate to="/workspace/sleact/channel/일반" />;

  return (
    <div className="max-w-[400px] mx-auto px-[20px]">
      <h1 className="flex flex-col items-center justify-center pt-[60px] pb-[20px]">
        <LogoChat color="#444791" />
        <span className="mt-[10px] text-primary text-[20px] font-bold">ReChat</span>
        <span className="blind">Slack</span>
      </h1>
      <TextField label="이메일 주소" type="email" value={email} onChange={onChangeEmail} />
      <TextField label="비밀번호" type="password" value={password} onChange={onChangePassword} />
      {logInError && <p className="mb-[20px] mt-[-10px] text-red-500 font-normal">로그인 실패</p>}
      <Button text="로그인" onClick={onSubmit} />
      <p className="mt-[10px] text-center">
        Slack을 처음 사용하시나요?
        <Link to="/sign" className="ml-[4px] text-[#004174]">
          회원가입
        </Link>
      </p>
    </div>
  );
};

로컬에서 돌릴 땐 아무 문제 없는데..

이건 에러 메세지도 보이지 않네요 어디를 봐야 하는 걸까요?

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

로그인 후의 /api/users에 유저 정보가 담겨있나요?

루룸님의 프로필 이미지
루룸
질문자

네 로컬 서버 환경에서 테스트 해보면 로그인 후의 /api/users 데이터를 console.log로 조회해보니 아래와 같이 담겨있습니다

image.png

근데 배포 환경에서는 login response 값에 유저 정보가 있어도 로그인이 되지 않아 /api/users 에는 false로 나오고 있습니다

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

로그인 후에 mutate()를 호출하면 자연스레 /api/users 요청이 다시 가게 됩니다. 네트워크 탭에서 응답을 확인하세요. 그리고 로그인 응답에 쿠키도 정상적으로 심어졌나 확인하시고요.

루룸님의 프로필 이미지
루룸
질문자

네트워크 탭에서 응답 확인한 내용입니다

login 요청에는 정보가 있으나 user는 여전히 false 이고

image.png

애플리케이션 탭에서도 쿠키가 생성되지 않았습니다

image.png

해당 부분 강의도 다시 들어봤는데 swr 사용하기(쿠키 공유하기) 강좌 10분 40초에서 지금 제 상황과 완전 동일한 에러에 대해서 설명해주신 내용대로 프론트 서버와 백엔드 서버의 도메인이 다를 경우 쿠키 생성도 안되고 데이터 전달도 안될 때 해야하는 설정 withCredentials: true 옵션도 되어 있습니다.

근데 지금 배포 환경에서는 프론트와 백엔드 도메인도 동일한 상황이라 위 이슈와는 연관은 없어보여서요. Login.tsx 코드를 수정해야할 게 있을까요?

<Login.tsx 코드>

const Login = () => {
  // useSWR은 get으로 요청한 데이터를 받아와서 저장한다.
  // mutate : 내가 원할 때 SWR 호출하기
  const { data, error, mutate } = useSWR('/api/users', fetcher, {
    dedupingInterval: 5000, // 주기적으로 호출하지만, dedupingInterval 기간 내에는 캐시에서 불러온다
  });
  const [logInError, setLogInError] = useState(false);
  const [email, setEmail, onChangeEmail] = useInput('');
  const [password, setPassword] = useInput<string>('');

  const onChangePassword = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setPassword(e.target.value);
    },
    [email, password, data],
  );

  const onSubmit = useCallback(
    (e: React.MouseEvent<HTMLButtonElement>) => {
      setLogInError(false);
      axios
        .post(
          '/api/users/login',
          { email, password },
          {
            withCredentials: true,
          },
        )
        .then(() => {
          mutate();
        })
        .catch((error) => {
          setLogInError(error.response?.status === 401);
        });
    },
    [email, password],
  );

  if (data) return <Navigate to="/workspace/sleact/channel/일반" />;

  return (
    <div className="max-w-[400px] mx-auto px-[20px]">
      <h1 className="flex flex-col items-center justify-center pt-[60px] pb-[20px]">
        <LogoChat color="#444791" />
        <span className="mt-[10px] text-primary text-[20px] font-bold">ReChat</span>
        <span className="blind">Slack</span>
      </h1>
      <TextField label="이메일 주소" type="email" value={email} onChange={onChangeEmail} />
      <TextField label="비밀번호" type="password" value={password} onChange={onChangePassword} />
      {logInError && <p className="mb-[20px] mt-[-10px] text-red-500 font-normal">로그인 실패</p>}
      <Button text="로그인" onClick={onSubmit} />
      <p className="mt-[10px] text-center">
        Slack을 처음 사용하시나요?
        <Link to="/sign" className="ml-[4px] text-[#004174]">
          회원가입
        </Link>
      </p>
    </div>
  );
};

 

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

login 요청의 headers에 set-cookie 헤더가 있는지 확인해보세요

루룸님의 프로필 이미지
루룸
질문자

image.png

login 요청 headers에 set-cookie 없습니다

루룸님의 프로필 이미지
루룸
질문자

해결했어요 백엔드쪽 코드에 문제가 있었네요

루룸님의 프로필 이미지
루룸
질문자

image.png

근데 로그인 후에 401 (Unauthorized) 에러가 뜨면서 갑자기 로그아웃 됐다가 알아서 다시 로그인 됐다를 반복하는데.. 이것도 쿠키 문제인가요?

채널리스트나 유저리스트도 간헐적으로 못 불러오는 것 같아요

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

네네 쿠키 문제들입니다. 쿠키가 전달되고 있는지 확인하셔야합니다

루룸님의 프로필 이미지
루룸
질문자

혹시 db 초기화는 어떻게 할 수 있나요? 배포환경에서 테스트로 생성됐던 유저 정보, 채팅 다 없애고 싶어서요

제로초(조현영)님의 프로필 이미지
제로초(조현영)
지식공유자

npx sequelize db:drop한 뒤에 다시 db:create 하시면 됩니다. seed도 하셔야 합니다.

루룸님의 프로필 이미지
루룸

작성한 질문수

질문하기