구름톤 트레이닝 풀스택 1기 WEB IDE 프로젝트 후기

걍걍규·2023년 11월 3일
1
post-thumbnail

사담


짧게 웹IDE를 만드는 과정은 쉽지 않았다
나는 물론 팀원들도 뭘만들어야하는거지 !?.. 하면서
구미가 당기지 않는것도 사실이였지만은 우리는 기획단계에서 매일 회의를 하며 아이디어를 합쳐갔다 ! 그러다가
우리팀은 알고리즘 스터디 ! 를 해왔기 때문에 프로그래머스와 같은 문제풀이를 하는 IDE를 만들기로 하였고 Figma로 프로토타입을 만들며 뼈대를 잡아갔다

필수 기능에 채팅구현이 있었기에 우리는 프로그래머스처럼 문제를 품과 동시에 코드를 함께 작성하고 서로 저장과 불러오기를 하며 공유도 하며 채팅까지 같이 할수 있는 서비스를 만들기로 했다 우왕키굿키

소스코드 링크
https://github.com/Goorm-Hongsam/HongsamIDE/blob/develop/front-ide/src/components/ide/JavaCodeEditor.js
깃허브 README.md
https://github.com/ganggyunggyu/HongsamIDE

기획


피그마로 레이아웃을 잡고 팀원과 공유하는 작업을 했다

RnR

주요 목표 및 하위 목표

  • Web IDE의 기본적인 틀과 사용자 인터페이스(UI) 설계
    • 복잡하지 않고 직관적인 레이아웃을 디자인하여 사용자가 각 기능을 빠르게 찾을 수 있도록 합니다.
    • 다양한 디바이스 및 화면 크기에서 웹 IDE가 잘 작동하도록 반응형 디자인을 구현하여 일관된 사용자 경험을 제공합니다.
  • 회원 가입 및 로그인 기능 구현
    • 회원 가입, 로그인, 비밀번호 재설정 등의 기능을 포함하는 사용자 관리 시스템을 설계하고 구현합니다.
  • 코드 편집기 기능 구현
    • 사용자가 손쉽게 새 파일 및 폴더를 생성하고 프로젝트 구조를 조직할 수 있는 기능을 구현합니다.
    • 파일 내 코드를 편집하고 저장할 수 있는 기능을 구현합니다.
  • 채팅 기능 구현
    • 다수의 사용자가 동시에 채팅할 수 있는 실시간 채팅 기능을 구현합니다.
    • 채팅 내용을 효율적으로 검색하고 필요한 정보를 빠르게 찾을 수 있는 메시지 검색 기능을 추가합니다.

나의 역할

  • IDE의 기본 툴과 사용자 인터페이스
  • 코드 편집기 기능 구현
  • 채팅기능
    • 채팅기능 자체를 구현하는 것은 아니고 구현된 채팅 컴포넌트를 문제풀이 화면에서 보여주는 역할
    • 문제 풀이 도중 url을 공유하면 비회원도 닉네임을 입력하고 문제풀이 및 채팅을 할수 있음

작업 과정

내가 맡은 파트가 아닌것은 영상만 첨부하겠습니다 !

로그인 회원가입 로그아웃

문제 필터링 및 진입


문제 최초 진입시 보여주는 화면

export const javaDefaultValue = (questionId) => {
  return `/////////////////////////////////////////////////////////////////////////////////////////////
// 기본 제공코드는 임의 수정해도 관계 없습니다. 단, 입출력 포맷 주의
// 아래 표준 입출력 예제 필요시 참고하세요.
// 표준 입력 예제
// int a;
// double b;
// char g;
// String var;
// long AB;
// a = sc.nextInt();                           // int 변수 1개 입력받는 예제
// b = sc.nextDouble();                        // double 변수 1개 입력받는 예제
// g = sc.nextByte();                          // char 변수 1개 입력받는 예제
// var = sc.next();                            // 문자열 1개 입력받는 예제
// AB = sc.nextLong();                         // long 변수 1개 입력받는 예제
/////////////////////////////////////////////////////////////////////////////////////////////
// 표준 출력 예제
// int a = 0;                            
// double b = 1.0;               
// char g = 'b';
// String var = "ABCDEFG";
// long AB = 12345678901234567L;
//System.out.println(a);                       // int 변수 1개 출력하는 예제
//System.out.println(b); 		       						 // double 변수 1개 출력하는 예제
//System.out.println(g);		       						 // char 변수 1개 출력하는 예제
//System.out.println(var);		       				   // 문자열 1개 출력하는 예제
//System.out.println(AB);		       				     // long 변수 1개 출력하는 예제
/////////////////////////////////////////////////////////////////////////////////////////////
import java.util.Scanner;
import java.io.FileInputStream;

/*
   사용하는 클래스명이 Solution 이어야 하므로, 가급적 Solution.java 를 사용할 것을 권장합니다.
   이러한 상황에서도 동일하게 java Solution 명령으로 프로그램을 수행해볼 수 있습니다.
 */
public class ${questionId} 	//문제에 따라 변할 수 있도록 수정 필요
{
	public static void main(String args[]) throws Exception
	{
		/*
		   아래의 메소드 호출은 앞으로 표준 입력(키보드) 대신 input.txt 파일로부터 읽어오겠다는 의미의 코드입니다.
		   여러분이 작성한 코드를 테스트 할 때, 편의를 위해서 input.txt에 입력을 저장한 후,
		   이 코드를 프로그램의 처음 부분에 추가하면 이후 입력을 수행할 때 표준 입력 대신 파일로부터 입력을 받아올 수 있습니다.
		   따라서 테스트를 수행할 때에는 아래 주석을 지우고 이 메소드를 사용하셔도 좋습니다.
		   단, 채점을 위해 코드를 제출하실 때에는 반드시 이 메소드를 지우거나 주석 처리 하셔야 합니다.
		 */
		//System.setIn(new FileInputStream("res/input.txt"));

		/*
		   표준입력 System.in 으로부터 스캐너를 만들어 데이터를 읽어옵니다.
		 */
		Scanner sc = new Scanner(System.in);
		int T;
		T=sc.nextInt();
		/*
		   여러 개의 테스트 케이스가 주어지므로, 각각을 처리합니다.
		*/

		for(int test_case = 1; test_case <= T; test_case++)
		{
		
			/////////////////////////////////////////////////////////////////////////////////////////////
			/*
				 이 부분에 여러분의 알고리즘 구현이 들어갑니다.
			 */
			/////////////////////////////////////////////////////////////////////////////////////////////

		}
	}
}
`;
};
  • questionId를 이용해 유저가 최초 진입한 문제인지 판단하여 해당 코드를 보여준다
  • questionId는 추 후에 채팅방을 만들기 위한 용도로도 사용된다
  • 서버로 코드를 보낼 때 출력 결과물을 이용하여 정답 유무를 결정할수 있다
  const fetchCode = async () => {
    setResult('코드 불러오기중...');
    await axios
      .post(
        'https://4s06mb280b.execute-api.ap-northeast-2.amazonaws.com/getcode',
        { questionId: questionIdParam, uuid: uuidParam }
      )
      .then((res) => {
        setCode(res.data);
        setResult('코드 불러오기 완료');
      })
      .catch((err) => {
        if (err.response.status === 500) {
          setCode(javaDefaultValue(questionIdParam));
          setResult(
            '주석을 보고 코드 작성 방법을 이해한 후에 아래의 타이머를 시작하여 문제를 풀어보세요 ! \n 아래의 타이머를 이용해서 내가 문제를 푼 동안 걸린 시간을 측정해보세요 !'
          );
        }
      });
  };
  • 최조 문제 진입시에는 서버에서 status코드를 500으로 보내주고 사용자에게는 터미널에 문제 푸는 방법을 설명해주는 텍스트를 보여주게 된다
  • 최초 진입 문제를 판별하는 방법은 유저 고유의 uuId와 questionId를 이용한다

문제 컴파일부터 채팅

업로드중..

문제 컴파일

  const compileCode = async () => {
    const code = editorRef.current.getValue();
    setResult('코드 컴파일 진행중 ...');
    await axios
      .post(
        'https://4s06mb280b.execute-api.ap-northeast-2.amazonaws.com/compile',
        {
          uuid: uuidParam,
          questionId: questionIdParam,
          requestCode: code,
          language: 'java',
        }
      )
      .then((res) => {
        setResult(res.data);
        if (res.data === '정답입니다.' || res.data === '틀렸습니다.') {
          setResultModalView(true);
        }
      })
      .catch((err) => {
        console.log(err);
      });
  };
import React from 'react';
import styled from './ResultModal.module.css';

export default function ResultModal({
  result,
  setResultModalView,
  isDarkMode,
}) {
  const closeModal = () => {
    setResultModalView(false);
  };
  return (
    <div
      className={`${styled.modalContainer} ${
        isDarkMode ? 'bg-zinc-800 text-white' : 'bg-white'
      } flex-col rounded-md absolute w-96 h-60 border z-10 p-3`}
    >
      <div className='w-full h-5'>
        <button
          onClick={closeModal}
          className='p-1 h-5 rounded-md flex items-center justify-center float-right'
        ></button>
      </div>
      <div className=' w-full mt-10 flex items-center justify-end flex-col'>
        <p className='text-3xl mb-7'>
          {result === '틀렸습니다.' ? '틀렸어요!😭' : '맞았어요!😆'}
        </p>
        <button className={`${styled.modalButton} mt-8 p-2 rounded-md`}>
          <a href='https://main.hong-sam.online/question'>다른 문제 풀러가기</a>
        </button>
      </div>
    </div>
  );
}
  • 작성한 코드는 서버로 전달되어 자바컴파일러를 거치고 실행 후에는 기존에 저장해놓은 여러가지의 테스트케이스가 입출력된다
  • 정답인지 오답인지 서버에서 보내주면 유저에게 그에 맞는 모달을 띄워준다
  • setState 함수를 props로 전달하면 좋지 않다는 이야기를 최근에 들었다 왜인지 알아봐야지

채팅

채팅의 경우는 uuId와 questionId를 합쳐 한 유저의 한 문제에만 독립적인 채팅방이 생기도록 만들수 있다

  const copyUrlToClipboard = () => {
    const currentUrl = window.location.href;
    navigator.clipboard
      .writeText(currentUrl)
      .then(() => {
        setUrlCopideView(true);
      })
      .catch((error) => {
        console.error('URL 복사 중 오류 발생:', error);
        alert('URL을 복사하는 중 오류가 발생했습니다.');
      });
  };
  • 유저의 입장에서 그냥 링크를 복사해서 넘길수도 있겠지만 더 쉽게 공유하기 위하여 버튼을 만들어주었다
 const fetchUserName = async () => {
    await axios
      .get('https://api.hong-sam.online/', { withCredentials: true })
      .then((res) => {
        if (res.data.status === 400 && sender) {
          return;
        } else if (res.data.status === 400) {
          alert(res.data.data);
          navigate(`/${uuidParam}/${questionIdParam}/guest`);
        } else if (res.data.status === 200) {
          setSender(res.data.data.username);
        }
      })
      .catch((err) => {
        console.log(err);
      });
  };
  • 유저의 이름을 전달받아야하는데 게스트의 경우에는 이름이 없다
  • 임시로 사용할 이름을 입력하기 위한 게스트페이지로 이동시켜준다
  useEffect(() => {
    fetchUserName();
    const tmpRoomId = uuidParam + questionIdParam;
    setUuid(uuidParam);
    setRoomId(tmpRoomId);
  }, []);
  • 이렇게 이름이 생긴 유저는 채팅방에 접근할수 있게 된다

대략적인 흐름을 보자면

  • 공유하기 버튼을 클릭하여 url을 복사할수 있고, url에는 uuId와 questionId에 대한 정보가 있어 채팅방 접근 권한이 생긴다
    • 회원인 경우 기존의 닉네임을 사용하게 되며 바로 진입하게 된다
    • 비회원인 경우 게스트 페이지로 이동하여 게스트 닉네임을 입력 후 접근하게 된다

이런 과정을 거쳐 채팅기능과 컴파일 기능을 모두 사용할수 있게 된다

저장과 불러오기를 이용한 코드 공유

위의 과정을 거쳐 게스트에게도 권한이 부여가 되었다면
두 유저는 코드 저장과 불러오기를 이용하여 각자의 코드를 공유할 수 있다

  const saveCode = async () => {
    const code = editorRef.current.getValue();
    setResult('코드 저장 중...');
    await axios
      .post(
        'https://4s06mb280b.execute-api.ap-northeast-2.amazonaws.com/savecode',
        {
          uuid: uuidParam,
          questionId: questionIdParam,
          requestCode: code,
          language: 'java',
        }
      )
      .then((res) => {
        setResult('코드 저장 완료');
      })
      .catch((err) => {
        console.log(err);
        setResult('코드 저장 실패 \n Run을 눌러도 코드 저장을 할수 있습니다.');
      });
  };
  • 작성한 코드를 서버로 보내서 컴파일은 하지 않고 저장만 한다
  • 혹시나 저장기능에 문제가 생길 경우를 대비하여 유저에게 Run 버튼을 눌러도 저장할수 있다는 점을 안내해준다
  • 성공적으로 저장했다면 게스트는 Pull 버튼을 눌러 코드를 받아볼 수 있게 된다
  const fetchCode = async () => {
    setResult('코드 불러오기중...');
    await axios
      .post(
        'https://4s06mb280b.execute-api.ap-northeast-2.amazonaws.com/getcode',
        { questionId: questionIdParam, uuid: uuidParam }
      )
      .then((res) => {
        setCode(res.data);
        setResult('코드 불러오기 완료');
      })
      .catch((err) => {
        if (err.response.status === 500) {
          setCode(javaDefaultValue(questionIdParam));
          setResult(
            '주석을 보고 코드 작성 방법을 이해한 후에 아래의 타이머를 시작하여 문제를 풀어보세요 ! \n 아래의 타이머를 이용해서 내가 문제를 푼 동안 걸린 시간을 측정해보세요 !'
          );
        }
      });
  };
  • 그렇다 ! 최초 진입 시 실행되는 코드와 동일하다
  • 서버에 저장되어있는 코드를 불러올 수 있도록 유저가 버튼을 눌러도 실행이 되도록 해주었다

이렇게 두 유저 혹은 다수의 유저는 채팅과 코드 공유 기능을 통하여 문제를 풀어나갈수 있게 된다 !
신나게 문제를 풀어보자

그 외 타이머 , 리사이징 , 다크모드

이 기능들은 필수 기능을 제외한 유저의 편의를 위하여 제작 된 기능이다

타이머

import React, { useState, useEffect } from 'react';

const Stopwatch = () => {
  const [isRunning, setIsRunning] = useState(false);
  const [elapsedTime, setElapsedTime] = useState(0);

  useEffect(() => {
    let intervalId;

    if (isRunning) {
      intervalId = setInterval(() => {
        setElapsedTime((prevElapsedTime) => prevElapsedTime + 1);
      }, 1000);
    } else {
      clearInterval(intervalId);
    }

    return () => {
      clearInterval(intervalId);
    };
  }, [isRunning]);

  const startStop = () => {
    setIsRunning(!isRunning);
  };

  const reset = () => {
    setIsRunning(false);
    setElapsedTime(0);
  };

  const formatTime = (timeInSeconds) => {
    const minutes = Math.floor(timeInSeconds / 60);
    const seconds = timeInSeconds % 60;
    return `${minutes.toString().padStart(2, '0')}:${seconds
      .toString()
      .padStart(2, '0')}`;
  };

  return (
    <div className='flex gap-3'>
      <div className=''>{formatTime(elapsedTime)}</div>

      <button onClick={startStop}>{isRunning ? 'Stop' : 'Start'}</button>
      <button onClick={reset}>Reset</button>
    </div>
  );
};

export default Stopwatch;
  • 단순하게 유저가 시간을 체크할수 있도록 구현한 스탑워치이다
  • 별도의 컴포넌트로 제작하여 사용하였고 유저는 하단 바에 위치한 타이머를 이용할수 있게 되었다

리사이징 & 다크모드

업로드중..

라이브러리의 존재를 알았다면은 훨씬 편하게 작업할수 있었을텐데 ..

  const [leftWidth, setLeftWidth] = useState(30); // 초기 왼쪽 너비 설정
  const [isResizing, setIsResizing] = useState(false);

  useEffect(() => {
    const handleResize = (e) => {
      if (!isResizing) return;
      const totalWidth = window.innerWidth;
      const newLeftWidth = (e.clientX / totalWidth) * 100;
      setLeftWidth(newLeftWidth);
    };

    const handleMouseUp = () => {
      setIsResizing(false);
      window.removeEventListener('mousemove', handleResize);
      window.removeEventListener('mouseup', handleMouseUp);
    };

    if (isResizing) {
      window.addEventListener('mousemove', handleResize);
      window.addEventListener('mouseup', handleMouseUp);
    }

    return () => {
      window.removeEventListener('mousemove', handleResize);
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isResizing]);

  const handleMouseDown = (e) => {
    e.preventDefault();
    setIsResizing(true);
  };
                <QuestionBar
                  leftWidth={leftWidth}
                  handleMouseDown={handleMouseDown}
                />

                <JavaCodeEditor
                  leftWidth={leftWidth}
                  isDarkMode={isDarkMode}
                  setIsDarkMode={setIsDarkMode}
                />
  • 리사이징이 필요한 두 컴포넌트에 너비를 관리해주는 State를 보낸다
      <div
        className='w-1 cursor-col-resize'
        onMouseDown={handleMouseDown}
      ></div>
  • 나같은 경우에는 좌측의 사이드바를 기준으로 삼았고 작은 div를 만들어주어서 유저가 리사이징을 할 수 있도록 해주었다
    • 해당 요소를 클릭하면 isResizing이 true로 변하여 마우스의 좌표를 감지하여 width를 조작해준다
  • window이벤트를 사용하며 하나하나 console.log를 찍어보았다 정말 힘든 노동이였고 해냈을때의 기분은 좋았다

터미널의 리사이징도 동일한 로직을 이용하였다 단지 너비 높이의 차이일 뿐이였다

  const [isDarkMode, setIsDarkMode] = useState(false);
  • 다크모드의 상태를 담고있는 스테이트를 필요한곳에 뿌려준다
            <>
              <div
                className={`flex ${
                  isDarkMode ? 'bg-zinc-800 text-white' : 'bg-white'
                }`}
              >
                <QuestionBar
                  leftWidth={leftWidth}
                  handleMouseDown={handleMouseDown}
                />

                <JavaCodeEditor
                  leftWidth={leftWidth}
                  isDarkMode={isDarkMode}
                  setIsDarkMode={setIsDarkMode}
                />
                <IdeBottomBar
                  sender={sender}
                  setSender={setSender}
                  isDarkMode={isDarkMode}
                />
              </div>
            </>
          }
        />
  • 상단바에는 다크모트 버튼이 존재한다
  • 두단계 이상 props를 전송하는 것은 좋은 방법이 아니기에 지금의 프로젝트에선 그것을 개선하였다
  useEffect(() => {
    if (!monaco) return;

    monaco.editor.defineTheme('tomorrow', TomorrowTheme);
    monaco.editor.defineTheme('tomorrowDark', TomorrowDarkTheme);

    monaco.editor.setTheme('tomorrow');

    isDarkMode
      ? monaco.editor.setTheme('tomorrowDark')
      : monaco.editor.setTheme('tomorrow');
  }, [monaco, isDarkMode]);
  • isDarkMode의 상태에 따라 변하는 한 예시이다
  • 코드 에디터의 테마가 isDarkMode의 상태의 따라 변환되는 것을 볼수 있다

후기

모든 글을 한 포스팅에 쓰다보니 렉도 걸리고 길어지고 여간 불편한게 아니다 나의 우여곡절이 모두 담기지 않았다!!
프로젝트를 진행하면서도 순간순간 마주치는 문제를 해결한다면 기록하는 습관을 들이자

시간이 지나니 기억은 휘발되기 마련이고 내가 마주쳤던 문제를 해결하는 방법이야 나의 몸에는 체득됐겠지만
나랑 일해보지 않는 사람 즉 기업은 그것을 알수가 없다

기획부터 빌드 배포까지 모든 과정을 밟아보며 팀원들과의 소통이 얼마나 중요한지 알았다
언제나 긍정적이되 낙천적이게는 굴지 말고 팀원에게 배우고 나도 팀원에게 유용한 지식을 전달해가며 함께 성장하겠다

글을 작성하고 있지만 해당 프로젝트가 끝나고 배운 많은것들을 적용하여 두개의 개인 프로젝트를 진행중이다
배웠으면 써먹고 써먹었으면 써먹은 것에 대해 돌아보고 다시 배우는 습관을 들이자 공부에는 끝이 없다

profile
안녕하시오?

0개의 댓글