학부 연구를 진행하던 도중, 유저의 로그인 구현에 대해 여러가지 문제를 겪었다. 

 

1. 일단 나의 애플리케이션에서는 SSO를 이용한 CAS를 이용해 로그인을 구현하였다.

2. 하지만 로그인은 보안 인증 문제가 까다롭기 때문에 HTTPS에서밖에 작동하지 않는다. 

 

그래서 HTTPS 프로토콜을 설정하려고 하다 보니, cors옵션을 설정 할 일이 있었다. 사실 그때는 뭐가 뭔지 모르고 그냥 구글링을 통해 얻은 정보로 소스코드만 작성했지만, 오늘 교수님과의 미팅중에 교수님이 내 코드를 보더니 "Why do you have cors options in your server side code? can you explain?" 나는 아무것도 답변할 수 없었다,,, 또 한번 깨달았다. 나는 지금 그냥 막연히 애플리케이션을 개발 - 배포하기보단, 애플리케이션을 개발 - 배포를 하면서 배우는중이라는 사실을 잠깐 망각했다. 그래서! 이번엔 CORS가 뭔지 보고 넘어가도록 하겠다.

CORS란 무엇인가?

CORS(Cross-Origin Resource Sharing)는 출처가 다른 자원들을 공유한다는 뜻으로, 한 출처에 있는 자원에서 다른 출처에 있는 자원에 접근하도록 하는 개념이다. 직역하면, 교차되는 출처 자원들의 공유다. 다른 출처에 있는 자원을 요청한다고 하면, 이를 교차 출처 요청이라고 부른다.

 

예전에는 CORS가 대중적이지 않았다. 하지만, 개발 시장이 빠르게 발전함에 따라, 내가 현재 접속한 애플리케이션에서 다른 애플리케이션과 상호작용할 일이 많아짐에따라 CORS가 등장하게 되었다.

 

더보기

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제입니다. 웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행합니다.

출처 : MDN

즉, CORS는 HTTP 통신을 요청할 때 HEADER에 CORS와 관련된 정보를 추가적으로 입력하여 다른 출처의 자원에 접근할 수 있도록 하는 것이다.

 

먼저, 출처라는 개념을 알아야 한다. 웹에서 '출처'란 무엇일까?

https://beomy.github.io/tech/browser/cors/

 

여기서 어디까지가 같다면 위에서 언급된 "한 출처"라고 할 수 있을까?

위의 요소 중, Protocol + Host + Port 3가지가 같으면 동일 출처(Origin)라고 한다.

 

다음은 다른 블로그에서 잘 정리된 테이블을 통해 간략히 동일, 다른 출처의 예시를 보고 넘어가 보자.

https://escapefromcoding.tistory.com/724

즉, 간단히 같은 출처와 다른 출처를 비교하자면,

  • 같은 출처 : 내가 접속한 사이트와 다른 사이트의 Protocol, Host, Port 가 전부 다 동일
  • 다른 출처 : 내가 접속한 사이트와 다른 사이트의 Protocol, Host, Port 중 하나라도 다른 경우

CORS의 동작 과정

  • CORS는 브라우저에 의해 동작한다
    1. 내가 접속한 사이트 (클라이언트)에서 다른 출처의 사이트 (서버)로 리소스를 얻기 위한 HTTP Request 를 보낸다
    2. 브라우저는 그 사이에서 Request Headers 에 Origin 이라는 필드에 요청을 보내는 출처 (내가 지금 접속하고 있는 사이트)를 보낸다
    3. 서버에서는 이 Request를 받아 Response Headers에 Access-Control-Allow-Origin
      이라는 필드에 접근이 허용되는 출처를 담아 응답한다.
    4. 브라우저는 해당 Response에 Access-Control-Allow-Origin 와 Request에 Origin 을 비교하여 이 응답이 CORS가 허용되는 응답인지 아닌지 결정한다.

자 그럼, 이제 실제 나의 프로젝트에서 Cors를 어떻게 설정 하는지 직접 코드로 확인해 보자.

const corsOptions = {
  origin: 'https://crescendo.cs.vt.edu:3000',
  optionsSuccessStatus: 200,
  credentials: true
};
app.use(cors(corsOptions));

위는 내 server.js의 corsOption을 설정하는 코드이다. 

  • origin : 이 옵션은 cors옵션을 허용할 도메인을 지정한다. 위의 경우, https://crescendo.cs.vt.edu:3000에서 오는 요청만 허용한다. 여러 도메인을 사용하고 싶을 경우, 배열을 사용할 수 있다. 예를들어, 2개의 도메인에서 들어오는 요청을 허용하고싶다면 아래와 같이 설정 할 수 있다.
origin: ['https://crescendo.cs.vt.edu:3000', 'https://anotherdomain.com']

만약, 모든 도메인에서의 요청을 허용하고싶다면, 

app.use(cors());

처럼 corsOptions라는 객체를 만들 필요도 없다.

  • optionSuccessStatus : 프리플라이트 요청(OPTIONS 메서드)에 대한 응답 상태 코드를 지정한다.
    일부 브라우저는 204 상태 코드를 제대로 처리하지 못할 수 있기 때문에, 200 상태 코드로 응답하도록 설정할 수 있다.
    기본값은 204 이지만, 여기서는 200으로 설정되어 있다.
  • Credentials: 클라이언트가 자격 증명(쿠키, HTTP 인증 정보, 클라이언트 SSL 인증서)을 포함한 요청을 허용할지 여부를 설정한다. true로 설정하면 자격 증명 정보를 포함한 요청이 허용된다.이 옵션이 true로 설정되면 Access-Control-Allow-Origin 헤더는 *로 설정할 수 없고, 특정 도메인을 지정해야 한다.

Trouble Shooting

문제 : 클라이언트에서 서버로 POST요청을 보내면 서버는 request를 받지도 않고, 당연히 request도 받지 않으니 response도 없었다. 포트 번호를 잘못 적었나 등등 여러가지를 면밀히 관찰했지만 이유를 찾아낼 수 없어 답답해 하던 도중, corsOption을 다시 봤다. 

 

나의 corseOption에서는 허용 도메인은 딱 하나이다. crescendo.cs.vt.edu:3000. 그런데, 생각해 보니 난 더이상 포트번호 3000번으로 접속 하고있지 않다는걸 깨달았다. 포트 번호를 지우니 POST요청도 정상적으로 받고, 응답도 제대로 해 주었다. 결국 내가 한참을 고민하던 이 문제는 cors때문이였다... 그래도 이 기회에 cors가 뭔지도 배웠으니 만족한다! 만약 여기서 문제가 발생하지 않았다면 난 cors가 뭔지도 모른채 그냥 코드만 찍어내고 말았을것이다. 

로컬에서 개발할땐 왼쪽 상단에 expandable한 메뉴 바가 보였다. 그런데 원격 서버로 화면을 띄워보니 메뉴 바가 보이지 않았던것!

 

이러한 이유는 몇가지가 있다. 잠재적인 이유는

 

1. 콘솔 오류 확인: 브라우저의 개발자 도구를 열고 콘솔에 오류가 있는지 확인하자. (JavaScript 오류나 리소스 로드 오류).

콘솔은 아무런 오류 메시지가 없다! 

 


2. 파일 경로 확인: CSS와 JavaScript 파일의 경로가 잘못된 상황 :  하지만 난 따로 CSS파일을 두지 않고, style속성을 이용하고 있으니 이 부분은 걱정 안 해도 될것같다.

 

3. 브라우저 캐시: 브라우저 캐시 문제일 수 있다.

  •  여기서 캐시가 또 다시 등장하니 다시 한번 오랜만에 상기하고 넘어가보자!
  •  캐시는 자주 사용되는 데이터나 리소스를 따로 저장해두는 곳으로, CPU에서 DRAM까지 내려가는 비용이 비싸기 때문에 캐시를 이용하면 원하는 자원들을 훨씬 더 빠르게 가져올 수 있다. 여기서 브라우저 캐시는 웹 페이지, 이미지, CSS, JavaScript 파일 등을 저장하여 다음 번에 동일한 리소스를 요청할 때 더 빠르게 로드할 수 있도록 한다.
  •  그렇다면, 이 캐시가 왜 메뉴버튼이 보이지 않는것에 대해 문제가 될까? - 오래된 캐시: 웹 페이지를 업데이트했지만 브라우저가 오래된 캐시된 버전을 사용하여 변경 사항이 반영되지 않을 수 있다.
    리소스 충돌: 동일한 파일 이름으로 다른 내용의 리소스를 업데이트하면, 브라우저가 새로운 리소스를 제대로 로드하지 못할 수 있다.

하지만 캐시를 지워도 문제는 해결되지 않았다... 그러면 element 탭을 좀 더 자세히 살펴 보도록 하자

 

개발자 도구를 열고, element탭을 열고, html태그들이 제대로 로드 되었는지 부터 확인한다.

그런데 HTML 태그는 제대로 로드되어 있지만, 화면에는 나타나지 않는 점이 이상했다. 아래와 같다.

 

보면 왼쪽 위에 div태그로 감싸진 태그를 볼 수 있고, 오른쪽 개발자 도구에서 저 fa-bars로 마우스를 올리면 저렇게 태그가 로드되어 있는걸 볼 수 있다. 

 

자 그럼, 이제 문제는 더 좁혀진다.

 

- 정상적으로 로드는 되었으나 보이지 않는 문제는 코드상의 문제일 확률이 높다.

 

---- 그런데 코드를 정말 하나하나 꼼꼼히 살펴 봐도 코드상의 문제는 아니였다. 그렇다면 ,,,

 

1. 개발자 도구를 열었을때, 로딩이 되지 않는 컴포넌트는 따로 없다. 모두 다 제대로 로딩이 되고 있다.

2. 코드상의 문제를 발견하지 못했다.

 

그렇다면 유력한 이유는 하나이다. 제대로된 라이브러리를 import하지 않았거나, 원격 서버에서 설치하지 않은것. 하긴, 로컬에선 되는데 원격에서 되지 않으면 코드상의 문제일리는 없다. 그래서 내가 쓰려던 메뉴바를 어디서 가져왔는지 다시 한번 확인해보자. 그래서 

 

import 'bootstrap/dist/css/bootstrap.min.css';
import '@fortawesome/fontawesome-free/css/all.min.css';

 

위 두 부분을 import 해주고 다시한번 서버를 재시작 했더니 메뉴 바가 보이기 시작했다.

 

 

이렇게! 그래서 다시 알아두어야 할 점은...

 

1. 개발자 도구를 열어 모든 컴포넌트가 렌더링 되는지 확인

2. 코드를 자세히 살펴보기

3. import나 라이브러리 설치를 까먹지 않았나 확인

 

 

일단 내가 만든 애플리케이션은 우리 학교 서버에 호스팅이 되어있다. 하지만 지금 살짝 어려움을 겪고있다. 

 

1. 프로토콜을 HTTPS로 변경하기(학교측 미들웨어 서비스 센터에서 cert 발급을 기다리는 중이다.)

2. 피드백을 보내고 받는 통신이 되었지만 여러 부분을 고치고 난 후 모종의 이유로 피드백을 보내고 받는 기능이 안 된다.

 

그래서 오랜만에 다시 백엔드 파일을 다시 살펴보도록 하자

require('dotenv').config();
const express = require('express');
const session = require('express-session');
const CASAuthentication = require('cas-authentication');
const cors = require('cors');
const mongoose = require('mongoose');

const app = express();
app.use(cors());
app.use(express.json());

app.use(session({
    secret: 'your_secret_key',
    resave: false,
    saveUninitialized: true
  }));

  const cas = new CASAuthentication({
    cas_url: 'https://login.vt.edu/profile/cas',
    service_url: 'http://cs.vt.edu:8080',  // 서비스 URL
    cas_version: '2.0',
    renew: false,
    is_dev_mode: false
  });

  app.use(cas.bounce);

const PORT = process.env.PORT || 8080;

const schemaData = mongoose.Schema({
    title: String,
    message: String,
}, {
    timestamps: true
});

const feedbackModel = mongoose.model("feedbacks", schemaData);
console.log('MongoDB URI:', process.env.MONGO_URI);
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
    .then(() => {
        console.log("Connected to db")
        app.listen(PORT, '0.0.0.0', () => {
            console.log(`Server is running on port ${PORT}`);
          });
    })
    .catch((err) => console.log(err))


app.post("/saveFeedback", async (req, res) => {
    try {
        console.log(req.body);

        if (!req.body.title || !req.body.message) {
            return res.status(400).json({ success: false, message: "Title and message are required fields" });
        }

        const data = new feedbackModel(req.body);
        await data.save();

        res.status(200).json({ success: true, message: "Data save successful" });
    } catch (error) {
        console.error(error);
        res.status(500).json({ success: false, message: "Internal server error" });
    }
});

app.get("/getFeedback", async (req, res) => {
    try {
        const data = await feedbackModel.find({}, { title: 1, message: 1 });
        res.status(200).json(data);
    } catch (error) {
        console.error(error);
        res.status(500).json({ success: false, message: "Internal server error" });
    }
});

애플리케이션 개발 초기 단계에 작성한 백엔드 파일이다. 아직 수정해야 할 부분도 많고 손 봐야 할 부분도 많다. 일단 1차원적으로 왜 통신안 되는지 살펴보자.

 

먼저, 여기서 Submit 버튼을 누르면 피드백이 서버로 보내지도록 되어있다. 하지만 현재 Submit버튼을 누르면, 아래와 같은 화면이 나온다....

일단 여기에는 몇가지 이유가 있을 수 있다. 하지만... 오늘은 다시 예전 내용을 복습하는 차원에서 코드를 하나하나 살펴보자

 

1. 필요한 모듈 불러오기.

require('dotenv').config();
const express = require('express');
const session = require('express-session');
const CASAuthentication = require('cas-authentication');
const cors = require('cors');
const mongoose = require('mongoose');

const app = express();
app.use(cors());
app.use(express.json());

 

 

  • require('dotenv').config(); : 이 라인은 .env 파일에 정의된 환경 변수를 로드하여 process.env 객체에 추가한다. 이렇게 하지 않으면 매번 환경변수를 export를 통해 일일히 설정해줘야한다. 
  • const express = require('express'); : Express는 Node.js를 위한 웹 프레임워크로, 서버를 쉽게 구축할 수 있도록 도와준다. express 모듈을 불러온다.
  • const session = require('express-session'); : 세션 관리를 위한 미들웨어로, 사용자의 세션을 관리할 수 있다.
  • const cors = require('cors'); : CORS(Cross-Origin Resource Sharing)를 설정하기 위한 미들웨어로, 다른 출처의 리소스에 대한 접근을 허용할 수 있다.
  • const mongoose = require('mongoose'); : MongoDB와 연결하고 상호작용하기 위한 ODM(Object Data Modeling) 라이브러리이다.

2.  앱 설정

const PORT = process.env.PORT || 8080;

const schemaData = mongoose.Schema({
    title: String,
    message: String,
}, {
    timestamps: true
});

const feedbackModel = mongoose.model("feedbacks", schemaData);
  • const PORT = process.env.PORT || 8080; : 서버가 실행될 포트를 설정한다. 환경 변수에 포트가 정의되어 있지 않으면 8080 포트를 사용한다.
  • const schemaData = mongoose.Schema({
        title: String,
        message: String,
    }, {
        timestamps: true
    }); : 피드백 데이터를 저장하기 위한 Mongoose 스키마를 정의한다. title과 message 필드를 포함하며, timestamps 옵션을 통해 생성 시간과 수정 시간을 자동으로 관리한다.
  • const feedbackModel = mongoose.model("feedbacks", schemaData); : 스키마를 기반으로 Mongoose 모델을 생성한다. 이 모델을 통해 MongoDB 컬렉션과 상호작용할 수 있다.

3. MongoDB와 연결

mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
    .then(() => {
        console.log("Connected to db")
        app.listen(PORT, '0.0.0.0', () => {
            console.log(`Server is running on port ${PORT}`);
        });
    })
    .catch((err) => console.log(err));
  • 환경 변수에 정의된 MongoDB URI로 MongoDB에 연결한다. 연결이 성공하면 서버를 지정된 포트에서 시작한다. 실패하면 오류를 출력한다.
    • 아까 맨 위에서 설정했던 환경변수가 여기서 쓰인다. 확장자가 .env인 파일안에  MONGO_URI환경변수가 정의 되어있다.
    • mongoose.connect는 비동기적으로 MongoDB에 연결을 시도한다.
    • 여기서 .then은 연결 성공시 실행되는 부분이다. 일단 편의를 위해 콘솔에 connected to db라고 표시를 해주고, 나머지 블록을 실행한다.
    • app.listen : Express 애플리케이션을 지정된 포트에서 수신 대기 하도록 한다.

4. 클라이언트로부터 받은 피드백을 저장할 라우트 설정

app.post("/saveFeedback", async (req, res) => {
    try {
        console.log(req.body);

        if (!req.body.title || !req.body.message) {
            return res.status(400).json({ success: false, message: "Title and message are required fields" });
        }

        const data = new feedbackModel(req.body);
        await data.save();

        res.status(200).json({ success: true, message: "Data save successful" });
    } catch (error) {
        console.error(error);
        res.status(500).json({ success: false, message: "Internal server error" });
    }
});
  • /saveFeedback 경로에 대한 POST 요청을 처리한다. 요청 본문에서 title과 message를 가져와 피드백 데이터를 저장한다. 필수 필드가 누락된 경우 400 상태 코드를 반환하고, 데이터 저장에 성공하면 200 상태 코드를 반환한다. 오류가 발생하면 500 상태 코드를 반환한다.

4. 피드백 조회 라우트 설정

app.get("/getFeedback", async (req, res) => {
    try {
        const data = await feedbackModel.find({}, { title: 1, message: 1 });
        res.status(200).json(data);
    } catch (error) {
        console.error(error);
        res.status(500).json({ success: false, message: "Internal server error" });
    }
});

/getFeedback 경로에 대한 GET 요청을 처리한다. 데이터베이스에서 모든 피드백 데이터를 조회하여 반환한다. 성공하면 200 상태 코드를 반환하고, 오류가 발생하면 500 상태 코드를 반환한다.

 

일단 여기까지 해 보았는데 전혀 문제되는 부분을 찾지 못했다. 아마 cors 설정이 잘못된거같은데, 어차피 프로토콜도 https를 써야 하니 지금 서버쪽 코드는 건들지 않도록 하겠다... https가 완전히 작동하면 그때 다시 한번 살펴 보도록 하겠다!

웹 호스팅, 배포 포스팅이랑 Docker 포스팅에서 언급했지만, 웹 애플리케이션을 개발하고 배포, 운용하는 과정은 상당히 복잡하다.

2024.04.11 - [Cloud Computing] - [Docker] Docker? 개념과 간단한 설명

 

[Docker] Docker? 개념과 간단한 설명

 

jghdg1234.tistory.com

2024.04.11 - [Cloud Computing] - 웹 서비스 배포, 호스팅

 

웹 서비스 배포, 호스팅

내가 처음 GUI수업을 들었을때가 생각난다. 처음 HTML로 못생긴 웹을 찍어내고, 그래도 내 딴엔 자료구조수업만 들었어서 처음으로 내 손으로 뭔가 웹사이트를 만든게 너무 신기했다. 그래서 이걸

jghdg1234.tistory.com

 

자 그럼, 이번에는 내가 만든 React feedback웹 애플리케이션을 Dockerize해 보도록 하겠다. 먼저 '도커화' 즉, dockerfile을 만들고, 이미지로 변환을 시켜야 컨테이너에서 운용 할 수 있다고 했는데, 어떻게 해야 할지 단계별로 살펴보자.

 

그 전에, DockerHub에 대해 정말 간단히 짚고 넘어가 보자. Dockerhub은 간단히 말해 우리가 흔히 생각하는 Appstore같은 것이다. 우리가 필요한 앱들을 우리 컴퓨터에 다운 받아서 쓰듯이, Docker Hub에서 우리는 program을 다운받는 대신 'image'라는것을 다운받는다. 그리고 program을 실행하면 process가 생성되는것 처럼, docker의 image을 실행하는것을 container 라고 부른다. 이렇게 비유한 이유는 뭐냐면, 하나의 프로그램이 여러개의 독립적인 프로세스를 가질 수 있듯이, 하나의 image는 여러개의 container를 실행 할 수 있다.

출처 : 생활코딩 Youtube

위 그림처럼 hub에서 이미지를 다운받는것을 'pull'이라고 하며, 그 이미지를 컨테이너에서 실행 시키는것을 'run'이라고 한다.

1. Docker를 설치한다.

Docker공식 홈페이지를 가면 OS에 따라 다운로드 가능한 버전들이 잘 나와 있다. 설치가 되면 터미널에 docker --version을 확인해 주는것을 항상 잊지 말자!

 

Docker가 정상적으로 설치 되었으면, 이제 나의 react app이 잘 돌아가는지 확인 해 보자. npm start를 했을때 정상적으로 페이지들이 렌더링 된다면 ok!

 

2. DockerFile 작성하기

이제, 가장 먼저 dockerfile을 작성해야 한다. 위의 Docker 포스팅에 가 보면 알겠지만, 먼저 dockerfile을 작성 한 후, 그 파일을 이미지로 변환 시켜야 한다.

 

먼저, 패키지 구조는 아래와 같다. 

이렇게 루트 디렉토리에 DockerFile이라는 파일을 하나 생성해 준다.

 

  • DockerFile 구성
FROM node:18-alpine

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm install 

COPY . .

RUN npm run build

EXPOSE 3000

CMD [ "npm", "start" ]

 

FROM node:18-alpine Node.js 버전 18이 설치된 alpine Linux를 기반 이미지로 사용


WORKDIR /src Docker 컨테이너 내에서 작업할 디렉토리를 app/로 설정


COPY package.json package-lock.json호스트 컴퓨터의 package.json와 package-lock.json 파일을 작업 디렉토리에 복사


RUN npm install 명령을 실행하여 필요한 패키지들을 설치


COPY . . 호스트 컴퓨터의 모든 파일을 작업 디렉토리에 복사


EXPOSE 3000 컨테이너의 3000번 포트를 외부에 노출. 애플리케이션이 해당 포트에서 실행될 것임을 나타냄


CMD [ "npm", "start" ] 컨테이너가 실행되면 npm start 명령을 실행

 

  • 컨테이너 이미지 생성
#형식 : 
$ docker build -t <컨테이너 이미지 이름> .

$ docker build . -t feedback-app

성공적으로 생성이 되면 위와 같은 터미널 화면을 볼 수 있을것이다.

 

여기서 한번 더 확인해야 할것이 있는데, 우리는 방금 image를 생성 했으므로 Docker Desktop으로 이동해 이미지가 잘 생성이 되었는지 한번 더 확인 해 준다.

 

 

3. 생성된 image를 기반으로 continaer 실행하기

 

기본적인 형식은 아래와 같다.

$ docker run -d --name <컨테이너 이름> -p 3000:3000 <실행할 이미지 이름>

 

 

이제, 여기 Docker Desktop에 가서 containers를 보면, 우리가 방금 실행한 컨테이너가 올라와 있는것을 볼 수 있다.

 

자 그럼, 이제 우리가 생성한 container가 localhost:3000에서 잘 돌아가는지 한번 확인 해 보자.

 

잘 돌아간다! 이제 DockerDesktop에서 실행중인 container를 정지버튼을 눌러 멈춰보고 실행해 보자. 그러면 이제 실행이 안 될 것이다.

 

짠! 

 

이렇게 간단하게 Docker를 이용해 React애플리케이션을 image화 하고, container를 실행 시켜 보았다. 아직은 초기 단계이지만, 나중에 가면 더 복잡해 질것이다.

이번에는 React를 사용한 웹 애플리케이션에서 페이지간의 라우팅을 어떻게 하는지 정말 간단히 짚고 넘어가겠다.

 

이번 프로젝트에서 요구하는 사이트를 예로 들어보자. 이번 페이지의 규모는 그렇게 큰 규모가 아니기 때문에(아직까지는) 일단은 2개의 페이지만 있다. 로그인페이지, 피드백 페이지.

 

로그인 구현은 아직 하지 못했기 때문에 이번에는 간단히 로그인 버튼을 누르면 피드백 페이지로 이동하도록 라우팅을 설정 하도록 하겠다.

 

리액트에서 라우팅이란, 

단일 페이지 애플리케이션(Single Page Application, SPA) 내에서 사용자가 다양한 페이지를 탐색할 수 있도록 하는 메커니즘을 말한다. 전통적인 웹 애플리케이션에서 페이지 간의 이동은 새로운 페이지 요청을 서버에 보내고, 서버는 새로운 페이지를 클라이언트에 전송하는 방식으로 이루어진다. 이 과정에서 페이지 전체가 새로고침되며, 사용자는 그동안 딜레이를 겪게 된다.

그러나 리액트와 같은 SPA 프레임워크나 라이브러리에서는 클라이언트 사이드에서 라우팅을 처리한다. 즉, 사용자가 링크를 클릭할 때마다 서버에 새 페이지를 요청하는 대신, 리액트가 미리 로드해둔 컴포넌트를 화면에 렌더링하여 페이지를 변경한다. 이 과정에서 실제 페이지 전환은 발생하지 않으며, URL만 변경됨으로써 사용자에게 여러 페이지가 있는 것처럼 느껴지게 한다.

 

리액트 라우팅의 장점:

1. 속도: 모든 리소스가 처음에 한 번만 로드되고, 필요한 컴포넌트만 선택적으로 업데이트되므로 페이지 전환 시 새로고침이 필요 없어 속도가 매우 빠르다.
2. UX: 페이지를 새로고침하지 않기 때문에 훨씬 향상된 UX를 가진다. 사용자는 끊김 없는 화면이동과 즉각적인 페이지 전환을 경험할 수 있다.
3. 효율적인 자원 사용: 필요한 컴포넌트만 로드하고 렌더링하기 때문에, 네트워크 사용량과 서버 부하가 감소한다.
4. 검색 엔진 최적화(SEO)**: 리액트 라우팅 라이브러리 중 일부는 서버 사이드 렌더링(SSR)이나 정적 사이트 생성을 지원하여 SEO 문제를 해결한다.

새로고침 여부:
리액트 라우팅을 사용하면, 사용자가 애플리케이션 내에서 페이지 간에 이동할 때 웹 페이지의 새로고침 없이 뷰가 변경된다. URL은 변경되지만, 이는 브라우저의 History API를 통해 관리되며, 실제로 페이지를 새로 불러오는 것이 아니라 애플리케이션 상태의 변경을 반영한다.

예제 설명:

예를 들어, 로그인 페이지에서 피드백 페이지로의 이동을 구현하려면, 리액트의 `react-router-dom` 라이브러리를 사용하여 라우팅을 설정한다. 로그인 버튼을 누르면, `useNavigate` 훅을 사용하여 피드백 페이지로의 경로를 지정할 수 있다. 이러한 방식으로, 리액트 애플리케이션에서는 사용자의 액션에 따라 끊김 없는 페이지 전환을 구현할 수 있다.


 

자 그럼, 이번 프로젝트에서의 예시를 한번 보자.

 

먼저, 올바른 디렉토리에서 npm install react-router-dom 를 입력해 router-dom을 설치해준다. 여기서 잘 확인해야 할 것은, 현재 설치된 react와 router-dom의 버전에서 불일치가 발생하지 않도록 해야한다.(나도 단지 이것때문에 30분동안 스택오버플로우를 뒤졌다...)

 

설치가 되었으면, package.json으로 이동해 dependencies안에 제대로 설치가 되었는지 확인한다.

 

  "name": "feedback_collection_web_app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.17.0",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "again": "^0.0.1",
    "axios": "^1.6.7",
    "bootstrap": "^5.3.2",
    "react": "^18.2.0",
    "react-bootstrap": "^2.10.0",
    "react-dom": "^18.2.0",
    "react-hook-form": "^7.51.2",
    "react-modal": "^3.16.1",
    "react-router-dom": "^6.22.3",
    "react-scripts": "5.0.1",
    "react-toastify": "^10.0.4",
    "web-vitals": "^2.1.4"
  },

 

이렇게  "react-router-dom": "^6.22.3" 설치가 된것이 보이면, router는 제대로 설치가 된 것이다. 먼저, login page에서 login이라는 버튼을 누르면 feedback page로 이동 할 수 있도록 코드를 살펴보자.

 

먼저, loginpage.js로 이동해 아래처럼 import를 해 준다.

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

 

 

React Router v5까지는 페이지 이동을 위해 useHistory 훅을 사용했었다. 그러나 v6부터는 useNavigate로 대체되어, 라우팅 및 페이지 이동 관련 기능을 좀 더 직관적이고 간결하게 사용할 수 있게 되었다. useNavigate 훅은 라우터의 history 스택에 직접적으로 접근하지 않고도 페이지 이동이나 뒤로 가기 등의 작업을 수행할 수 있게 해준다.

 

 

 

자 그럼, 우리는 login버튼 하나만 가지고 있으므로, 그 버튼에 대한 핸들러 함수를 추가한다. 단, 핸들러를 추가 하기 전, navigation을 담당 할 상수를 선언 해 준다.

const navigate = useNavigate();

 

그 다음 핸들러 함수를 작성 해 준다.

 

    const handleLogin = (event) => {
        event.preventDefault();
        toast.info('Navigating to VT CAS login page...');
        // login stuff going on
        setTimeout(() => {
          navigate('/FeedbackForm');
        }, 2000)
    };


...



          <Button onClick={handleLogin} style={{ backgroundColor: 'maroon', borderColor: 'maroon', 
          padding: '10px 20px', fontSize: '16px' }} type="submit">CAS Login</Button>

 

나는 핸들러 함수에 setTimeout함수를 추가해 toast로 유저들에게 로그인 페이지로 이동 중이라고 알려주었다. 저기서 위에서 선언한 상수 'navigate'에다가 ( 를 열고, import한 FeedbackForm이라는 컴포넌트를 넣어준다. 이렇게 하면 버튼이 클릭되었을때, navigation 상수가 알아서 페이지 간 전환을 담당 할 것이다. 이렇게 보니까 정말 간단하다.. 나는 의존성부터 시작해서 여러가지 애를 먹었지만 다음에는 이런 일이 없었으면 좋겠다.

 

이렇게 loginPage.js에서 라우팅을 담당하는 함수를 작성 한 후, React의 엔트리 포인트인 index.js에서 이제 페이지들을 어떻게 렌더링 할지 결정 해야한다.

 

현재 나의 index.js는

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
// Bootstrap CSS
import "bootstrap/dist/css/bootstrap.min.css";
// Bootstrap Bundle JS
import "bootstrap/dist/js/bootstrap.bundle.min";

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

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

그냥 이렇게 <App /> 컴포넌트를 받아 렌더링을 하기 때문에 나는 App.js를 수정해야 한다.

import FeedBackForm from './pages/FeedBackForm';
import LoginPage from './pages/loginPage';
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';


function App() {
  return (
    <React.StrictMode>
    <BrowserRouter>
    <Routes>
      <Route path="/" element={<LoginPage />} />
      <Route path="/FeedbackForm" element={<FeedBackForm />} />
    </Routes>
    </BrowserRouter>
    </React.StrictMode>
  );
}

export default App;

 

보다시피 위에 BrowserRouter, Routes, Route를 import 한 후, 제일 상단을 <BrowserRouter>로 감싼다. 그 후, <Routes> 태그 안에는 우리가 이동하고싶은 페이지들의 경로를 <Route>태그 안에 넣어 준다. 여기서 짚고 넘어가야 할 것이 있다.

 

먼저, element = {<>}안에 들어 가야 할 내용은 우리가 import한 페이지 컴포넌트들을 넣어주면 된다.

 

그리고, 

위에서 보이는 <Route path ="/"는 루트 경로로 매핑 한다는 뜻이다. 즉, path에서 / 를 가진 컴포넌트가 웹을 실행할때 가장 먼저 렌더링 될 페이지이다. 보통의 웹 페이지라면 서비스를 이용하기 전에 로그인을 먼저 해야함으로 나는 login page를 루트 경로로 설정 해 줬다.(이걸 몰라서 화면에 아무것도 뜨지 않아 또 새벽 두시에 스오플을 뒤졌다...)

 

이렇게 모든 과정을 마친 후,

이렇게 페이지간 이동을 할 수 있게 되었다!

리액트로 앱을 만들어 본 사람이라면 'npm run'을 하고 웹을 띄워 본 적이 있을 것이다. 그런데 리액트에서 디폴트로 제공하는 favicon은 아래와 같은 모양이다.

 

그런데 뭔가 저 파비콘을 변경하지 않고 그대로 두면 뭔가 조잡한(?) 느낌이 난다... 뭔가 방금 웹을 만들고 손 보지 않은 기분 + 그 웹을 대표하는 로고가 없으면 뭔가 professional한 느낌도 나지 않고,,, <- 그냥 내 개인적인 생각이다

 

그래서 오늘은 리액트로 웹 서비스를 만들 때, 어떻게 favicon을 바꿀 수 있는지 알아보도록 하겠다. 생각보다 간단하다!

 

먼저, 구글에 favicon generator라고 치면 여러개의 사이트가 나온다. 그 사이트에 원하는 이미지를 불러 넣으면, 알아서 favicon.ico로 변환 해 줄것이다. 그러면 이제 내가 작업중인 디렉토리로 가서 'public' 디렉토리로 간다.

 

 

 

여기에다가 방금 변환시킨 favicon.ico를 넣어준다. 기존에 있던 디폴트 favicon은 그대로 둬도 되고, 삭제해도 된다. 아무튼 저 public 디렉토리에 favicon을 넣고, index.html로 들어간다.

 

저기 보면 <link rel ="icon"으로 시작하는 부분이 보일것이다. 사실 favicon.ico의 이름을 바꾸지 않았다면 저건 그냥 그대로 둬도 된다. 하지만 favicon.ico가 아니라 다른 이름으로 썼다면 저 위에 이름도 바꿔줘야 한다!

 

저렇게 설정해주고, 기존에 열려있던 웹을 닫고 다시 npm start를 통해 앱을 열어보면

짜잔! 이렇게 바뀌어 있다. 너무 간단한 일이다.

 

오늘은 react favicon 변경 방법에 대해 알아 보았다.

처음 웹의 폰트를 변경하려고 구글링을 했을땐 ftt파일로 변경하는법, css로 변경하는법 등등 여러가지 방법이 있었다. 하지만 불필요한 html태그를 쓰지 않고(즉, html태그를 쓰먄 그에 맞는 태그에 css로 스타일링을 해줘야 한다) 자바스크립트의 컴포넌트 내에서 직접 style을 정의하여 font를 설정 해 보았다. 물론 각 방법마다 장단점은 있겠지만, 제일 직관적이고 편한 방법을 소개하겠다.

 

 

아래는 현재 내가 구축해놓은 정말 간단한 로그인 화면이다. 아직 로그인의 인증절차는 구현하지 않았고, 일단은 UI적인 부분부터 손 보기로 했다. 그런데 아래를 보자. 처음 보는 사람이 이 사이트를 본다면 주저없이 '스캠' 이라고 의심 할 것이다. 왜냐면 너무 사이트가 허무맹랑 하기 때문이다. 첫번째, 맨 위의 저 글씨체를 보면 뭔가 너무 없어보인다. 물론 글씨체를 바꿔도 없어 보이긴 하겠지만 우리의 목표는 글씨체이니 일단 글씨체부터 바꿔보도록 하자.

자 그럼, 기본 글씨체 말고 다른 글씨체로 한번 바꿔보자.

 

먼저, 구글폰트에 들어가 원하는 글꼴들을 찾아본다.

나는 이 폰트로 결정했다. 자 그럼 이제 get embeded code 버튼을 클릭한다. 클릭하면 아래와 같은 화면이 보일텐데

 

 

여기서 저기 위에 'Embeded code in the <head> of your html' 부분의 html태그로 감싸진 코드들을 복사한다.

 

복사 후, 디렉토리 최상단에 있는 index.html파일의 <head> 태그 안에 복사한 코드를 붙여넣기 해 준다.

 

그 다음, 로그인 페이지를 담당하는 loginPage.js 파일로 가 타이틀을 담당하는 부분을 찾는다.

바로 여기!

 

    const headerBar = (
        <Navbar style={{ backgroundColor: '#880000', fontFamily: 'Righteous'}}}>
          <Navbar.Brand href="#home">
            Virginia Tech CS Capstone Feedback Page
          </Navbar.Brand>
        </Navbar>
      );

 

<Navbar style에다가 fontFamily를 적용하고, 단순히 '' 안에다가 <head> 태그 안에 넣었던 태그를 넣어주면 된다. 이렇게 하면

이렇게 글씨체가 바뀌었다! 

 

복잡하게 css에서 또 . # 태그를 이용해서 컴포넌트를 가져오지 않아도 되고,(물론 규모가 커지면 CSS에서 관리하는게 유지보수 하기 더 편할 수 있다) 직관적인 이름으로 간단하게 폰트를 바꿀 수 있다. 

 

딱딱한 느낌을 지우기 위해 아래처럼 조금만 CSS적인 요소를 추가해주면

    const headerBar = (
        <Navbar style={{ backgroundColor: '#880000', fontFamily: 'Righteous', padding: '10px', borderRadius: '10px' }}>
          <Navbar.Brand href="#home" style={{ color: '#fff', fontSize: '24px' }}>
            Virginia Tech CS Capstone Feedback Page
          </Navbar.Brand>
        </Navbar>
      );

 

짜잔! 

이렇게 바뀌었다. 아직도 뭔가 조잡해보이지만 점점 더 꾸며 나가면 괜찮지 않을까 싶다.

 

 

연구 프로젝트 설명:
Research Project (high level) Description: Motivation: Many undergraduate courses, especially senior-level courses, involve project-based learning. For example, the Computer Science department at VT has many capstone courses which are designed “to synthesize and integrate skills and knowledge acquired throughout the CS undergraduate curriculum, and which includes a significant design experience, where teamwork and written and oral communication are a key part of that design experience [link].“In such project-based courses, typically, professors ask students to give milestone presentations so that professors can check their progress, give feedback to each team, and allow students to see how other teams have made progress. By nature, getting feedback from milestone presentations can benefit the team in gaining ideas for improving the project and successful final artifacts. Some instructors use a peer-assessment system so that peers’ evaluations will collectively determine the team’s grade for the presentation. Peer assessment has proven to have positive learning outcomes. Importantly, peer assessment is a social process; some claim that anonymity might have advantages. While it can be beneficial for students to learn from evaluating other students’ work, give constructive feedback, and receive feedback from others, students typically do not feel comfortable allowing their peers to determine their grades [link]. Students’ grading may be inconsistent across individuals, and some can even game the system in favor of their grades. If we only include peer-review feedback without grading, eliciting participation can be another challenge for instructors. PI’s teaching practice to address the problem: PI Lee has developed and applied a novel peer-review feedback system in his Creative Computing Studio capstone course that he taught four times. In this class activity, the peer-assessment system has been modified; the class gives feedback to a presenting team, and the presenting team evaluates the feedback they received from their peers based on the quality of the feedback. For example, Whenever there is a group presentation, the PI asks everyone in the class to give feedback to the presenting teams. For example, suppose there are ten teams of four students in a project-based class. In that case, each student will have to pay attention to everyone else’s presentations (9 presentations) except their own presentation and leave feedback in an online survey. The online survey has three types of feedback entries: “I like (positives),” “I wish (improvements),” and “What if (suggestions)” sections. Once those responses are collected, the PI anonymizes the feedback and gives feedback to the team. Then the team has to evaluate each peer’s feedback based on simple criteria. (e.g., is it specific? Is it justified? is it actionable? Is it positive? [link]). Then one student will get their grades from 9 different teams, eventually becoming the student’s final grade for the assignment (5% of the course grade). This crowdsourced peer-review system has been developed over four iterations of the same course and is now used consistently. Here’s a sample spreadsheet that one team received from their peers. The PI anticipates multiple benefits of this system. First, students can practice giving constructive feedback, as their grade depends on the quality of the feedback. Second, each team may take the crowdsourced feedback more seriously than the instructor’s feedback as they find some common theme that emerges from multiple students. Third, the entire class can develop a sense of community as they give feedback to their peers over the semester, witnessing the progress of each team as a result of forced peer-review systems.

 

간단히 말해, 현재 Virginia Tech 의 학부 Computer Science 시스템은 졸업 전 필수 이수 과목인 'Capstone Course'를 듣도록 하고 있는데, 이 과목은 이론보다는 프로젝트 기반의 수업이다. 각 팀은 4명정도의 팀원으로 구성되어 있으며, 한 수업당 4-50명정도의 학생이 강의에 참여한다. 이 수업은 위에서 언급했듯 프로젝트 베이스이며, 각 팀들은 학기 시작부터 프로젝트를 구상하고, 모델링, 프로토타입, 와이어 프레임 등 여러가지 milestone을 거쳐 최종 프로젝트 발표에 이르는것이 Virginia Tech의 Capstone과정 중 하나이다. 이 과정중, 각 팀들은 서로 다른 팀의 발표 내용이나 프로젝트 진행 상황에 대해서 피드백을 남겨야 하는데, 그 피드백은 여러가지로 나뉘어서 채점된다. 

  •  피드백이 정당한가?
  • 피드백이 설득력이 있는가?
  • 피드백이 발표자의 요점을 잘 파악했는가?
  • 피드백이 너무 포괄적이지 않고 디테일 한가?

등등으로 채점을 해야 한다.

 

하지만 현재 시스템에서는 이러한 모든 일들을 Google Docs를 통해 하나하나 일일히 해야 한다. 교수님은 이러한 것들을 위해 학생들을 하나하나 각기 다른 팀에 배분해야 하며, 학생들이 피드백을 남길 때마다 교수님이 직접 체크해야 한다. 이것은 상당히 불편한 일이다. 대부분의 서비스들이 연동 되는 현 시점에 이렇게 귀찮은 방법으로 하는건 비효율적이다.

 

그래서!

이번 프로젝트의 목적은 위의 일을 번거롭지 않게 처리 할 수 있는 캡스톤 피드백 시스템을 개발 해내는 것이다. 이 시스템은 유저들간의 피드백을 주고 받을 수 있어야 하며, 일일히 채점하는 수고를 덜도록 학생들이 피드백을 평가하면 자동으로 채점이 되어 데이터베이스에 점수를 남기는 것이다.

 

여기까지가 기본적인 이번 연구 주제의 내용이며, 앞으로 추가 사항이 있을때마다 추가 해 나가도록 하겠다.

+ Recent posts