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

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

sunnnwo님의 프로필 이미지

작성한 질문수

파이썬/장고로 웹채팅 서비스 만들기 (Feat. Channels) - 기본편

채팅방 참여자 목록 노출

도커와 연동 관련 질문드립니다.

작성

·

84

0

version: '3.8'

services:
  postgres:
    image: postgres:14
    env_file: .env
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend

  django:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "8000:8000"
    volumes:
      - ./backend:/app
      - static_volume:/app/myproject/staticfiles
      - ./frontend:/app/myproject/myapp/static/myapp
      - media_volume:/app/myproject/media
    depends_on:
      - postgres
      - redis
    environment:
      - PYTHONUNBUFFERED=1
      - DJANGO_SETTINGS_MODULE=myproject.settings
    env_file: .env
    networks:
      - frontend
      - backend

  redis:
    image: redis:6
    networks:
      - backend

  nginx:
    build: ./nginx
    volumes:
      - media_volume:/usr/share/nginx/html/media
      - static_volume:/usr/share/nginx/html/static
      - ./frontend:/usr/share/nginx/html/frontend
    ports:
      - "8443:8443"
    depends_on:
      - django
    networks:
      - frontend
      - backend

  adminer:
    image: adminer:latest
    restart: always
    ports:
      - "8080:8080"
    networks:
      - backend
    depends_on:
      - postgres

volumes:
  postgres_data:
  static_volume:
  media_volume:

networks:
  backend:
  frontend:

docker-compose 설정인데요, 저희 팀원이 만들때 templates 폴더를 안쓰고 전부 .js 파일로 만들어놨습니다. 이런 경우 채팅 모듈을 추가하려고하면 어떤 방식으로 해야할까요. 강의외적인 부분 질문드려서 죄송합니다.

답변 3

1

이진석님의 프로필 이미지
이진석
지식공유자

장고 템플릿은 SSR (Server Side Rendering). 즉 서버 단에서 HTML 응답을 만들 때 사용합니다.

UI를 JS로 구성하셨다면, 이 코드는 서버 단이 아닌 클라이언트 단에 JS 파일이 다운로드되어 동작되어야 합니다. 서버 단에서 코드가 수행되는 것이 아니라, 단순히 서버는 이 JS 파일들을 서빙(serve)만 할 뿐, 브라우저 단에서 이 JS파일들을 로딩하여 수행되어야 합니다.

JS로 구성하신 부분이 SPA (Single Page Application)인가요? frontend 부분 만으로 UI가 뜨는 상황인것이죠? 그럼 frontend 서비스와 django 서비스는 별개의 서비스로 동작하는 것이구요. django 서비스는 API 서비스로만 사용하시는 것으로 보여집니다. SPA라면 장고 서비스와 별개의 서비스로 생각하시고, nginx와 같은 별도의 웹서비스로 띄우시고 장고 웹서비스와 도메인만 맞춰주시면 됩니다.

JS 파일이 다운로드되는 주소와 장고 API 주소가 포트 번호만 다른 경우라면, 세션 활용을 위해 장고 단에서는 CORS 설정과 JS 단에서 http 요청을 보낼 때에는 credentials: "include" 설정을 추가해주시면 쿠키가 공유되어 세션인증까지 같이 활용하실 수 있습니다. 아래는 다른 강의인 장고/리액트 강의의 슬라이드입니다. 참고하세요.

image.png

그런데, 지금처럼 구성하시면, JS 코드에 UI 코드가 이리저리 스파게티처럼 뒤섞여있기에 UI를 관리하기가 무척 어려워보입니다. :-(

SPA로 하실 것이라면, 리액트나 vue.js와 같은 UI 라이브러리의 도움을 받으시는 것이 나으실 수도 있습니다.

더 궁금하신 부분이 있으시면 help@pyhub.kr 메일로 주시면, 확인해드리겠습니다. :-)

sunnnwo님의 프로필 이미지
sunnnwo
질문자

답변 감사합니다. 제가 좀더 고민해보고, 시도해보겠습니다.

간단하게 생각했던건데 생각보다 복잡하더라고요.ㅎㅎ

고민해도 안되면 이메일로 질문드리겠습니다. 감사합니다.

1

이진석님의 프로필 이미지
이진석
지식공유자

안녕하세요.

전부 js 파일로 만드셨다는 것이 어떤 의미인지, 그 의도를 잘 모르겠습니다. 샘플로 js 파일의 코드가 어떻게 구성되어있고, 각 파일들을 어떻게 활용코자 하시는지 알려주시면, 그에 맞춰 가이드를 드리겠습니다. :-)

sunnnwo님의 프로필 이미지
sunnnwo
질문자

import { checkIsTournament, loadContent, replaceContent } from "./utils.js";
import { generateNavigator } from "./nav.js";
import { getUserInfo } from "./utils.js";
import { loadProfilePage } from "./profile.js";
import { loadStatsPage } from "./stats.js";
import { addNavigatorEventListeners } from "./eventListener/navigator.js";
import { translation } from "./translate.js";
import { loadPreparationSimpleMatchGamePage } from "./preparation-simple-match-game-page.js";
import { loadPreparationTournamentGamePage } from "./preparation-tournament-game-page.js";
import { loadContinueOrNewTournamentPage } from "./continue-or-finish-page.js";
import { loadOnlineGamePage } from "./online.js";


let globalSocket = null;

export async function loadHomePage()
{
	let userInfo = await getUserInfo();

	// Vérifiez si une connexion WebSocket existe déjà
	if (!globalSocket || globalSocket.readyState === WebSocket.CLOSED) {
		let jwtToken = localStorage.getItem('jwt_token');
		globalSocket = new WebSocket(`wss://localhost:8443/ws/online/?token=${jwtToken}`);
		console.log("Nouvelle connexion WebSocket créée.");
		setupWebSocketListeners(globalSocket, userInfo);
	} else {
		console.log("Réutilisation de la connexion WebSocket existante.");
	}
	
	// Si la connexion est ouverte, envoyez un message de connexion
	if (globalSocket.readyState === WebSocket.OPEN) {
			globalSocket.send(JSON.stringify({
			event: "connect",
			username: userInfo.username
		}));
	} else {
		// Gérer le cas où la connexion est en train d’être ouverte
		globalSocket.onopen = () => {
			globalSocket.send(JSON.stringify({
			event: "connect",
			username: userInfo.username
			}));
		};
	}

	let homeHTML = generateHomePageHTML(userInfo);

	loadContent(document.getElementById("app"), homeHTML, "home", true, 'Home Page', translation, addNavigatorEventListeners, addEventListenerHomePage);
}

window.addEventListener('popstate', async function(event) {
	if (event.state && event.state.page) {
		if (this.window.location.pathname === "/home")
			loadContent(this.document.getElementById("app"), event.state.page, '', false, 'Home Page', translation, addNavigatorEventListeners, addEventListenerHomePage);
	}
});

export function addEventListenerHomePage()
{
	let switchPageToProfilPage = document.getElementById("profil-button");
	if (switchPageToProfilPage)
	{
		switchPageToProfilPage.addEventListener('click', function (event) {
			event.preventDefault();
			loadProfilePage();
		});
	}

	let switchPageToStatsPage = document.getElementById("stats-button");
	if (switchPageToStatsPage)
	{
		switchPageToStatsPage.addEventListener('click', function (event) {
			event.preventDefault();
			loadStatsPage();
		});
	}
	
	let switchPageToTournamentPage = document.getElementById("tournament-button");
	if (switchPageToTournamentPage)
	{
		switchPageToTournamentPage.addEventListener('click', async function (event) {
			event.preventDefault();
			let checkIsTour = await checkIsTournament();
			if (checkIsTour)
				loadContinueOrNewTournamentPage();
			else
				loadPreparationTournamentGamePage();
		});
	}

	let switchPageToOnlinesPage = document.getElementById("online-button");
	if (switchPageToOnlinesPage)
	{
		switchPageToOnlinesPage.addEventListener('click', function (event) {
			event.preventDefault();
			loadOnlineGamePage();
		});
	}

	let switchPageToSimpleMatchPage = document.getElementById("simple-match-button");
	if (switchPageToSimpleMatchPage)
	{
		switchPageToSimpleMatchPage.addEventListener('click', function (event) {
			event.preventDefault();
			loadPreparationSimpleMatchGamePage("multiplayer", "multiPlayerTwo");
		});
	}

	let switchPageToDoublesPage = document.getElementById("doubles-button");
	if (switchPageToDoublesPage)
	{
		switchPageToDoublesPage.addEventListener('click', function (event) {
			event.preventDefault();
			loadPreparationSimpleMatchGamePage("multiplayer", "multiPlayerFour");
		});
	}
}

function setupWebSocketListeners(socket, userInfo) {
    // WebSocket événement `onmessage`
    socket.onmessage = (event) => {
        const message = JSON.parse(event.data);
        console.log("Message reçu du serveur :", message);

        // Gestion de l'événement `ping`
        if (message.event === "ping") {
            console.log("Ping reçu, envoi du Pong.");
            socket.send(JSON.stringify({ event: "pong" }));
        }

        // Mise à jour de l'état de connexion
        if (message.isConnect !== undefined) {
            console.log(`L'utilisateur ${userInfo.username} est maintenant ${message.isConnect ? "en ligne" : "hors ligne"}.`);
        }
    };

    // WebSocket événement `onclose`
    socket.onclose = (event) => {
        console.warn("WebSocket connection closed :", event);
    };

    // WebSocket événement `onerror`
    socket.onerror = (error) => {
        console.error("WebSocket error :", error);
    };
}


function generateBodyHomePageHTML(username, profile_photo)
{
	let imgProfile;
	if (profile_photo)
		imgProfile = profile_photo;
	else
		imgProfile = "../images/profile-logo-white.png";
	let profilStr = "Profile";
	if (username !== "")
		profilStr = username;
	return `
		<div class="message-change-orientation">
			<h1 style="font-size: 25px; text-align: center;" data-translate-key="messageChangeOrientation"></h1>
			<i class="fa-solid fa-rotate" style="font-size: 50px; text-align: center;"></i>
		</div>
		<div class="flex-container">
				<button id="profil-button" class="flex-item box1">
					<img class="img-box1-2" src="${imgProfile}" alt="logo profile" width="70" height="70">
					<div class="item-name">
						<h1 data-translate-key="profile"></h1>
					</div>
				</button>
				<button id="stats-button" class="flex-item box2">
					<img class="img-box1-2" src="../images/stat-logo-white.png" alt="logo settings" width="70" height="70">
					<div class="item-name">
						<h1 data-translate-key="statistics"></h1>
					</div>
				</button>
				<button id="tournament-button" class="flex-item box3">
					<img class="img-box3-4" src="../images/trophy-logo-white.png" alt="logo solo" width="175" height="175">
					<div class="item-name">
						<h1 data-translate-key="tournament"></h1>
					</div>
				</button>
				<button id="online-button" class="flex-item box4">
					<img class="img-box3-4" src="../images/online-logo-white.png" alt="logo solo" width="175" height="175">
					<div class="item-name">
						<h1 data-translate-key="online"></h1>
					</div>
				</button>
				<button id="simple-match-button" class="flex-item box5">
					<img class="img-box5-6" src="../images/multiplayer-logo-2-players-white.png" alt="logo 2 players" width="100" height="100">
					<div class="item-name">
						<h1 data-translate-key="simple"></h1>
						<h3 data-translate-key="one-vs-one"></h3>
					</div>
				</button>
				<button id="doubles-button" class="flex-item box6">
					<img class="img-box5-6" src="../images/multiplayer-logo-4-players-white.png" alt="logo 4 players" width="100" height="100">
					<div class="item-name">
						<h1 data-translate-key="doubles"></h1>
						<h3 data-translate-key="two-vs-two"></h3>
					</div>
				</button>
		</div>
	`;
}

export function generateHomePageHTML(userInfo)
{
	let nav = generateNavigator();
	let body = generateBodyHomePageHTML(userInfo.username, userInfo.profile_photo);

	return (nav + body);
}

home.js 파일일입니다.

import { loadAuthentificationPage } from "../auth.js";
import { loadHomePage } from "../home.js";
import { reorderLanguages, translation, changeLanguage } from "../translate.js";
import { loadFriendsPage } from "../friends.js";
import { removeUserInPlayerOnline } from "../utils.js";

export function addNavigatorEventListeners()
{
	const lang = localStorage.getItem('language') || 'en';
	switchPageToHome();
	switchPageToLogout();
	switchPageToFriends()
	reorderLanguages(lang);

	const languageSelect = document.getElementById('language-select');
	if (languageSelect)
	{
		languageSelect.addEventListener('change', (event) => {
			changeLanguage(event.target.value);
			translation(event.target.value)
		});
	}
}

function switchPageToHome()
{
	let switchPageToHome = document.getElementById("home-button");
	if (switchPageToHome)
	{
		switchPageToHome.addEventListener('click', function (event) {
			event.preventDefault();
			loadHomePage();
		});
	}
}

function switchPageToFriends()
{
	let switchPageToFriends = document.getElementById('friends-button');
	if (switchPageToFriends)
	{
		switchPageToFriends.addEventListener('click', function (event)
		{
			event.preventDefault();
			loadFriendsPage();
		});
	}
}

function switchPageToLogout()
{
	let switchPageToLogout = document.getElementById("logoutLink");
	if (switchPageToLogout)
	{
		switchPageToLogout.addEventListener('click', async function (event) {
			await removeUserInPlayerOnline();
			localStorage.removeItem('jwt_token');
			event.preventDefault();
			loadAuthentificationPage();
		});
	}
}


네비게이터에 chat 버튼을 넣어 추가하려고 하는데, 저희 팀이 장고내에서 템플릿을 사용하고 있지 않습니다. 이런 경우는 어떻게 기능을 추가해야할까요. 답변 주셔서 감사합니다.

0

인프런 AI 인턴님의 프로필 이미지

안녕하세요, 인프런 AI 인턴입니다.

Docker와 연동하여 Django 프로젝트를 구성하신 것을 보니 복잡해 보이지만 잘 정리되어 있습니다. Team에서 templates 대신 JavaScript 파일을 사용하시기 때문에, 다소 혼란스러울 수 있으실 것 같습니다. 이 사안과 관련하여 조금 설명을 드리자면, Django는 기본적으로 템플릿 시스템을 제공하여 페이지를 렌더링합니다. 그러나 JavaScript를 활용하여 SPA(Single Page Application)나 CSR(Client Side Rendering)을 구현하는 경우, 템플릿 대신 React, Vue.js 같은 프레임워크를 사용하는 것도 보편적입니다.

이런 경우, 채팅 모듈을 추가하시려면 다음 사항을 고려해 보시면 좋겠습니다:

  1. React 혹은 Vue.js와 같은 JavaScript 프레임워크를 사용 중인지 확인하십시오. 만약 그렇다면, 해당 프레임워크의 컴포넌트를 활용하여 채팅 기능을 구현할 수 있습니다.

  2. RESTful API로 채팅 모듈을 구축하십시오. Django에서는 Django REST Framework를 사용하여 API 엔드포인트를 생성하고, JavaScript로 비동기적으로 데이터를 주고받을 수 있습니다.

  3. WebSocket을 활용한 실시간 기능 구현. Django Channels를 활용하여 WebSocket을 지원하면, 채팅과 같은 실시간 기능을 추가할 때 유용합니다. Redis를 사용할 경우, 메시지 브로커 역할을 할 수 있어 Channels와의 연동이 편리합니다.

지금 다루시는 환경은 프런트엔드와 백엔드가 분리된 구조이므로, 채팅 모듈을 구현할 때는 위 구조에 맞게 백엔드 API와 프런트엔드 JavaScript 코드 간의 통신을 잘 디자인해야 합니다.

아래 유사한 질문들을 참고하시면 도움이 될 수 있습니다:
- Docker와 Django를 통한 배포와 연동 방법
- Nginx + Django + Channels 배포 시 이슈 해결

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.

sunnnwo님의 프로필 이미지

작성한 질문수

질문하기