@senspond
>
이번 글은 이 웹사이트(RgbitCode)에 명령프롬프트(터미널) UI 를 개발했던 과정을 정리해보았습니다.
아직도 손볼곳들이 굉장히 많지만 블로그에 필요한 기본적인 기능들은 대충 만들기는 했는데, 정작 블로그에 글들이 별로 없는 것 같다. 글을 쓰는 행위는 정말 귀찮지만.. 블로그의 본질은 기능이 아닌 컨텐츠 내용물 이기 때문에 앞으로 조금씩이라도 내용물들을 채워나가야 할 것 같다. 이번 글은 이 웹사이트에 명령프롬프트(터미널) UI 를 개발하며 만났던 이야기를 적어보려고 한다.
서버 개발자라면 친숙한 명령 프롬프트에서 영감을 받아 웹사이트에 실험적인 UI를 구현해보고자 하였고 RgbitCode 웹 사이트에 적용된 개발자 취향에 맞춘 명령 프롬프트라 할 수 있다. 애플 디자인과 Unix Shell 명령어를 모방해서 구현을 하고자 했다. 개발 초기에는 블로그 개발한다는 목적도 상실해버리고 이런 웹사이트의 개발자 컨센을 표현하는데 오히려 더 많은 시간이 걸려버린것 같다. 그런데 아직 완벽하지 않다.
웹 사이트 디자인은 애플을 많이 모방했다. 미적 감각이 제로에 가까운 백엔드 서버 개발자인 내게는... 모방 만이 살길이었다. 누가 그러지 않았는가.. 모방은 창조의 어머니라고 말이다.
터미털 아이콘을 클릭 하거나 마우스를 hover 했을때 표기되는 단축키를 입력을 통해 열 수 있도록 구현을 했다.
터미널은 RgbitCode Termial 이란 이름으로 모달 창으로 열리게 된다.
맥북에서 터미널 열였을때 그 느낌이 나오도록...
export const LAYOUT = {
DOCK_POSITION : {
TOP : "top",
BOTTOM : "bottom",
LEFT : "left",
RIGHT : "right",
},
THEME : {
DARK : 'dark',
LIGHT : 'light'
},
DEVICE : {
PC : "pc",
MOBILE : "mobile"
},
CODE_THEME : {
XONOKAI : "xonokai",
DARCULA : "darcula",
DUOTONE_EARTH : "duotone-earth",
DUOTONE_SEA : "duotone-sea",
A11Y_DARK : "a11y-dark"
}
}
/**
* 레이아웃 상태
*/
interface LayoutStateType {
dockPosition : string, // dock 패널 위치
isTerminalOpen : boolean, // 터미널 오픈여부
theme : string, // 테마
codeTheme : string, // 코드테마
device : string // device
}
// 초기 레이아웃 상태
const initialState : LayoutStateType ={
dockPosition : LAYOUT.DOCK_POSITION.LEFT,
isTerminalOpen : false,
theme : LAYOUT.THEME.LIGHT,
codeTheme : LAYOUT.CODE_THEME.XONOKAI,
device : LAYOUT.DEVICE.PC,
}
export const slice = createSlice({
name : 'layout',
initialState,
reducers :{
dockPosition(state, action) {
state.dockPosition = action.payload
},
codeTheme(state, action){
state.codeTheme = action.payload
},
terminalOpen(state) {
state.isTerminalOpen = true
},
terminalClose(state) {
state.isTerminalOpen = false
},
theme(state, action) {
state.theme = action.payload
},
device(state, action){
state.device = action.payload
}
},
});
cd 명령을 통해 페이지 전환 등 기능을 구현하다보니 터미널이 열려있는지 닫혀있는지를 상태로 체크할 필요가 있었다. dockPosition, theme 등도 명령 프롬프트로 명령어입력 지원을 할 것이고 RgbitCode 전용 명령어를 모르는 사람들을 위해서는 상단 설정 UI를 통해 마우스클릭으로도 변경할 수 있고 변수가 공유되어야 하기 때문에, 전역으로 상태를 관리할 필요가 있었다. 그래서 Redux를 통해서 관리를 하도록 만들었다.
const CodeTerminal = ()=>{
const pathName = usePathname();
const getRoutePath = (url: string)
=> decodeURI(decodeURIComponent((url?.replaceAll(/\?.+/g, '') as string)));
const commandHandler = async (text:string) => {
let response;
let argsIndex = text?.indexOf(' ');
let command = argsIndex !== -1 ? text.substring(0, argsIndex).trim() : text.trim();
let useMultiLine = false
switch (command) {
case 'pwd':
response = decodedPath
break;
case 'exit':
dispatch(layoutAction.terminalClose())
break;
default:
response = '존재하지 않는 명령어를 입력하셨습니다 : ' + command;
break;
}
}
if (response) Terminal.send(response);
};
useEffect(() => {
Terminal.on(commandHandler);
setPath(getRoutePath(pathName) + ' $')
return () => {
Terminal.off(commandHandler);
};
}, []);
}
명령프롬프트에 입력한 명령어를 ' ' 공백으로 구분해 맨앞에 있는 명령어코드 값을 가져오고 각각의 명령어코드 마다 case문으로 분기를 타게 되고 해당 명령어에 맞는 기능을 적용하였다.
초기 버전에는 구현하는것에만 초점을 두어 되는대로 만들다보니 유지보수 하기가 너무 어려운 문제가 있어서 많은 부분이 수정이 되었다. 하지만, 앞으로 업그레이드 해가며 명령어가 추가되고 변경될 것을 고려해보면 아직도 좀더 깔끔하게 정리할 필요가 있다.
const getRoutePath = (url: string)
=> decodeURI(decodeURIComponent((url?.replaceAll(/\?.+/g, '') as string)));
NextJs 13 버전 이상 App 라우팅 기반 방식에서는 useRouter 가 아닌 next/navigation 의 usePathname 을 사용해서 경로를 가져와야 한다. 그런데 nextjs 의 usePathname 을 통해 경로를 표시하거나 경로로 이동할때 한글이 포함된 경우 인코딩되어 깨져버리는 현상이 발생했다. 그래서 위와 같이 작성하여 해결하였다.
리눅스 cd 명령어를 그대로 모방하는것이 목적이었는데, 웹사이트에 구현을 하려니 몇가지 난항들이 있었다.
예를 들어 현재 경로가 /blog 인 경우에서 cd senspond 라고 입력하면 /blog/senspond 로 페이지가 라우팅 되어야 하고 cd .. 라고 입력하면 / 페이지로 라우팅 되어야 한다. 그리고 cd /senspond 로 입력하면 절대 경로로 /senspond 로 이동되어야 한다. 그런데 blog/senspond 는 존재하는 페이지여도 /senspond 는 존재하지 않는 페이지이기에 그에 대한 처리가 되어야 한다.
정리를 해보자면 다음과 같다.
1. 현재 라우팅 경로를 알고 있어야 한다.
2. cd .. cd senspond cd /senspond cd ./senspond 를 구분해서 절대경로, 상대경로를 처리해 이동경로를 만들어 내야한다.
3. 올바른 명령어 패턴인 경우 절대경로,상대경로 구분하여 페이지 이동처리
cd "" cd '' cd "blog 등과 같이 패턴에 맞지 않도록 입력한 경우에 대한 처리필요.
이동하려는 경로가 존재하지 않는 페이지인 경우 처리필요.
app 라우팅의 경우 app 디렉토리 밑에 not-found.tsx 를 정의하면 존재하지 않는 페이지의 경우 해당페이지로 이동하게 된다. 현재 그런 방식으로 not-found 404 페이지로 이동하게 구현되어있는데, 터미널 명령을 자주 사용해보니 빈번하게 404 페이지를 보여줄 수가 있다보니 추후 터미널에서 메시지로 보여주도록 개선할 여지가 있다. 아래처럼 말이다
먼저 간단하게 생각해보면 모든 페이지에 대응하는 케이스를 하드코딩으로 작성하는 방법을 떠올릴 수 있었다. 그런데 향후 페이지들이 추가될 때마다 계속 수정해줘야 하는 코드가 되버리기 때문에 동적으로 자동으로 표기되도록 만들고 싶었다.
정리해보면 다음과 같다.
1. ls 명령어 입력시 현재 페이지의 디렉토리 경로를 알아 내야한다
2. 현재 페이지의 디렉토리 내에 존재하는 페이지들을 탐색해서 가져와야한다.
3. 가져온 목록을 터미널 화면에 출력해줘야 한다.
현재 페이지의 디렉토리 경로는 서버에 접근하는 방법밖에 떠오르지 않았다. 그래서 nextjs 의 server api 를 작성하였다. 현재 페이지의 디렉토리 내의 존재하는 페이지들을 재귀적으로 탐색하여 리스트에 담아 반환하는 API 이다.
import fs from "fs"
import { NextResponse } from "next/server";
import path from "path";
export async function GET(request: Request) {
const list = [] as string[];
const appPattern = new RegExp("(.*\/app)(\/.*)", "g");
const pagePattern = new RegExp("(.*)\/(page.*)", "g");
const inspectionFindFile = (destPath : string) => {
try {
fs.readdirSync(destPath, { withFileTypes: true })
.forEach((file) => {
const path = `${destPath}/${file.name}`;
const dPath = path.replaceAll(appPattern, "$2");
if(dPath.includes("page.")){
list.push(dPath.replace(pagePattern, "$1"))
}
if (file.isDirectory()) {
inspectionFindFile(path);
});
} catch(err) {
console.error('Read Error', err);
// TODO : error response
}
}
inspectionFindFile(path.join(__dirname, "..",".."));
console.log(list)
return NextResponse.json({
list
});
}
ver0.1 에서는 nextjs page 라우팅 방식을 사용했는데, ver0.2 로 와서 app 라우팅 방식으로 변경하고 코드를 수정하여 적용했더니
ls 명령에 목록이 중복으로 출력되는 버그가 발생하였다.
출력이 터미널 세로 길이를 초과해버는 경우 터미널 창의 스크롤이 자동으로 내려가서 명령어 입력 Inputbox에 포커스가 맞춰줘야 하는데, 현재 버전에서는 그렇게 되어있지 않다. 생각보다 자잘하게 신경써야 할 것들이 있다.
웹 사이트에 독특한 명령프롬프트(터미널)을 구현했던 과정들을 정리해보았습니다. 아직 전체적으로 보완해야 될 부분들이 남아 있네요. 앞으로를 기대해주세요.
안녕하세요. Red, Green, Blue 가 만나 새로운 세상을 만들어 나가겠다는 이상을 가진 개발자의 개인공간입니다.
현재글에서 작성자가 발행한 같은 카테고리내 이전, 다음 글들을 보여줍니다
@senspond
>