udemy로 react 강의를 듣다가 진도율이 50% 까지 찬 걸 알게되었다. 지금까지 공부한게 머릿속에 있는게 맞는지 확인도 할 겸 간단한 프로그램 하나 만들어 보는게 많은 도움이 될 것 같았다.
그래서 당장 내가 배운 걸 활용해서 만들어 볼만한게 뭐가 있을지를 생각해봤다.
처음에는 무료 API 사이트를 사용해 영화 검색 프로그램 비슷한 걸 만들까 생각했다. 강의 내용 중에서도 있었던 내용이기도 하고 서버 통신도 좀 더 익숙해지지 않을까 싶기도 했다.
하지만 기왕 만들거라면 내가 실제로 평소에 쓸만한 프로그램을 만들고 싶다는 생각에 일단은 보류하기로 했다.
그래서 평소에 쓸만한 프로그램이면 뭐가 있을까 하다가 영단어장이 떠올랐다. 그런데 머릿속에서 구상해보니 영단어장은 아무래도 페이지간 변화가 잦을 것 같아서 지금보단 react router까지 진도를 나간 후에 만드는게 좀 더 맞을 것 같아서 조금 미루기로 했다.
그래서 결국에 결정한 주제는 뽀모도로 타이머.
12년 동안 교육청의 의무교육을 받은 토종 한국인으로 살면서 50분 공부 10분 휴식이라는 사이클 없이는 버틸 수 없는 몸이 되어버린 나는 고등학교를 졸업하고도 공부를 할 때 휴식시간과 집중시간을 분류해서 공부를 한다.
이런 타입의 시관 관리법을 보통 뽀모도로라고 하는데 프란체스코 시릴로라는 대학생이 1980년대에 제안한 시관 관리 방법론으로 토마토 스파게티를 의미하는 뽀모도로를 만들 때 사용하던 토마토 모양의 타이머에서 본 따 설계되었다.
- 토마토 모양의 뽀모도로 타이머 -
현재에도 많은 뽀모도로 타이머 어플리케이션들이 존재하고 나도 실제로 Focus-to-do라는 어플을 자주 사용하고 있다. 따라서 이러한 뽀모도로 타이머를 만들어보자라는 생각을 하게 되었다.
사실 처음부터 프로그램을 만들어보는게 처음이라 그런지 어떤것부터 시작해야 할 지가 막막했다. 일단 create-react-app
은 했는데 이제부터 뭘 해야할 지가 생각이 안났다.
그래서 우선 구현 기능 목록 부터 생각해봤다.
1. 시간 데이터를 가진 수정 가능한 컴포넌트 리스트 구현
2. 타이머 구현
3. 컴포넌트 데이터 타이머에 적용시키기
크게 보자면 위 3가지가 주요 기능이였다.
위의 3가지를 작성하고 떠오른게 아무래도 전역 상태관리를 사용해야 할 것 같은 생각이 났다.
단순히 입력값을 받고 타이머를 작동시킨다면 prop의 이동이 크지 않겠지만 todo 리스트의 컴포넌트에서 타이머까지 데이터를 이동시켜야 하니 상태관리 라이브러리를 하나 선택하는 것이 더 나은 방향이라 생각하였고 최근에 공부하였기도 하고 store 분리가 간편한 Redux Toolkit을 사용하기로 결정하였다.
사실 기능 구현 말고도 UI를 어떻게 만들지도 난관이었다.
Figma를 지금부터 배울 수도 없고 어떡하지 생각하다가 그냥 단순하게 만들기로 했다.
UI를 단순화 하는 대신에 그나마 보기에 좀 괜찮았으면 싶어서 예전에 보고 이쁘다고 생각했던 뉴모피즘을 적용시켜보기로 했다.
사실 뉴모피즘은 나같은 초보가 만들기에는 살짝 무거운 느낌이 있었는데 다행히도 뉴모피즘 css를 생성해주는 사이트가 있어서 많은 도움을 받았다.
- 뉴모피즘 CSS 마크업 생성 사이트 neumorphism.io -
기본 마크업을 하면서 디바이스 반응을 어떻게 해야할 지를 생각해보았다. 처음엔 미디어쿼리를 사용해서 모바일 반응을 할 예정이었지만, 어차리 데스크탑과 모바일의 레이아웃 구조가 바뀌는 것도 아니니 굳이 미디어쿼리를 사용해서 해상도에 따른 반응을 할 필요는 없다고 느껴졌다.
대신에 컴포넌트들의 width
를 설정할 땐 뷰포트를 기준으로한 vw
를 적용시켜서 반응하도록 작성하였다.
- 해상도별 타이머 레이아웃 -
타이머 코드는 기본적으로 00 : 00
형식의 문자열로서 관리되기에 이를 어떻게 카운트 할지도 고민이었다. 구글에 그냥 타이머 예시를 긁어올까 생각하다가 그냥 타이머 카운트 전용 유틸리티 함수를 작성하기로 하였다.
// counter.js
const counter = (time) => {
const [min, sec] = time.split(" : ");
if (+sec > 0) {
let changeSec = `${+sec - 1 + ""}`;
if (sec.length === 1 || sec[0] === "0" || sec === "10")
changeSec = `0${changeSec}`;
return `${min} : ${changeSec}`;
}
if (+sec === 0) {
let changeMin = `${+min - 1 + ""}`;
if (min.length === 1 || min[0] === "0" || min === "10")
changeMin = `0${changeMin}`;
if (+min > 0) return `${changeMin} : ${"59"}`;
else return "00 : 00";
}
};
나도 안다, 코드에서 구린내가 난다는걸.
하지만 나름대로 나에게는 최선이었다.
사실 이 부분에서 가장 많은 애를 먹었다. JS에서 타이머를 구현한다면 setInterval
을 사용하는건 사실상 필수불가결한데 문제는 리액트의 특성에 있었다. 타이머를 정지시키려면 clearInterval
로 인터벌을 제거해야하는데 그렇다면 대체 어떻게 setInterval
을 지정할 것인가가 가장 큰 고민이었다.
최종적으로 선택한 방법은 Redux의 상태로 인터벌을 부여하기.
컴포넌트 내에서 인터벌 생성시 인터벌이 계속 반복 생성되는 문제도 있었고 다른 컴포넌트에서 인터벌을 제거할 방법이 도저히 떠오르지 않아 위와 같은 방법을 선택했다.
인터벌이 부여되는 위치인 타이머 컨트롤러 컴포넌트에서 상태로 인터벌을 dispatch
하는 방법으로 작성하였다.
// TimerController.js
const startHandler = () => {
if (currentTime === "00 : 00") return;
dispatch(
timerActions.setCustomInterval(
setInterval(() => {
dispatch(timerActions.count());
}, 1000)
)
);
};
//timerSlice.js
const timerSlice = createSlice({
name: "timer",
initialState: {
settedTime: "",
time: "00 : 00",
interval: 0,
},
reducers: {
setCustomInterval: (state, actions) => {
state.interval = actions.payload;
},
clearCustomInterval: (state) => {
clearInterval(state.interval);
},
count: (state) => {
state.time = counter(state.time);
if (state.time === "00 : 00") {
const alram = new Audio(alramSound);
alram.loop = false;
alram.play();
state.active = false;
clearInterval(state.interval);
state.time = state.settedTime;
}
},
},
);
코드로 설명하자면 dispatch
의 action
을 아예 setInterval
의 반환값을 보내면 timerSlice
의 상태 중 하나인 interval
에 해당 값이 부여된다. 이는 Redux에서 관리하므로 렌더링과는 상관 없이 항상 같은 메모리 주소에 위치하며 slice 내에서 직접 참조할 수도 있다.
이후에 타이머가 00:00이 되거나 STOP 혹은 RESET 등 인터벌의 제거가 필요한 시점에서 clearCustomInterval
액션을 통해서 인터벌의 직접적인 조작이 가능하게 만들었다.
사실 이것보다 좋은 방법이 있을 것 같지만 나의 두뇌와 현재 내가 아는 지식으로서는 이정도가 한계였다.
그리고 카운트가 00 : 00이 될 시 알람을 재생되고 기존 선택한 타이머의 시간으로 초기화 되도록 구현하였다.
알람 사운드는 아이폰의 기본 벨소리 중에서 선택하였다.
리스트의 경우 가장 기본적인 localhost
를 사용하였다. todo 목록이 업데이트 될 때마다 로컬에 새로운 데이터를 JSON 형식으로 저장하는 방식으로 작성하였다.
그리고 페이지 진입시 useEffect
를 통해 최초 상태 업데이트를 실시하는데 받아온 값을 반복문으로 추가하는 대신에 아예 초기 데이터를 바꿔버리는 액션 함수를 작성하였다.
// todoSlice.js
const todoSlice = createSlice({
name: "todo",
initialState: {
todos: [],
editTargetId: null,
},
reducers: {
add: (state, action) => {
const newTodo = action.payload;
state.todos.push(newTodo);
localStorage.setItem("todos", JSON.stringify(state.todos));
},
delete: (state, action) => {
state.todos = state.todos.filter((todo) => {
return todo.id !== action.payload;
});
localStorage.setItem("todos", JSON.stringify(state.todos));
},
setEditedId: (state, action) => {
state.editTargetId = action.payload;
},
edit: (state, action) => {
state.todos = state.todos.map((todo) => {
if (todo.id === action.payload.id) {
todo = action.payload.changedTodo;
}
return todo;
});
localStorage.setItem("todos", JSON.stringify(state.todos));
},
setup: (state, action) => {
state.todos = action.payload;
},
},
// TodoList.js
useEffect(() => {
const initTodos = JSON.parse(localStorage.getItem("todos"));
if (initTodos) dispatch(todoActions.setup(initTodos));
}, [dispatch]);
처음에 사용자에게 값을 입력받을땐 단순하게 prompt
를 사용하여 값을 받도록 구현하였다. 하지만 모달을 사용하는 편이 좀 더 직관적이고 사용성이 좋을 것이라 생각해서 모달을 통해 값을 받아오기로 계획을 바꿨다.
모달에도 상태를 설정하여 모달의 mode
상태를 만들고 모드에 따라 액션이 달라지도록 구현하였다.
- Modal Layout -
Todo를 변경 시키기 위해서 모달을 띄울때 입력창에 기존 데이터를 입력시켜놓고 싶었다.
그래서 처음에 제목,분,초에 대한 useRef
를 생성 후 인풋 태그에 할당했더니 참조한 current
값이 undefined
가 나왔다.
알아보니 DOM이 생성되기 전에 참조를 읽어서 해당 DOM이 존재하지 않으니 나타나는 현상이었다.
그렇기에 최초 마운트 시에만 실행하도록 useEffect
에 의존성을 추가하지 않고 참조된 DOM에 기존 데이터를 할당하도록 구현하였다.
// TodoEditor.js
const titleRef = useRef();
const minRef = useRef();
const secRef = useRef();
const edittedTime = useSelector((state) => state.timer.settedTime);
const currentTitle = useSelector((state) => state.timer.title);
const [currentMin, currentSec] = edittedTime.split(" : ");
useEffect(() => {
if (modalMode === "edit") {
titleRef.current.value = currentTitle;
minRef.current.value = currentMin;
secRef.current.value = currentSec;
setTitle(currentTitle);
setMin(currentMin);
setSec(currentSec);
}
}, []);
종료시 알람음이 나온다.
처음으로 맨땅에 프로그램을 구현해보다보니 가장 강하게 든 생각이 하나 있다.
세상사 내맘대로 되는게 하나도 없다!
정말로 하나를 수정하면 다른곳이 터지고 하나를 수정하면 다른 곳이 터져나가니 정말로 신경쓸게 한두가지가 아니었다.
그렇다보니 구현에 급급해지고 반사작용으로 점점 코드에서 구린내가 심해지는걸 느꼈다.
강의에서 배운 useMemo
나 useCallback
이나 커스텀 훅 같은걸 써볼까 싶었지만, 잘못 썼다간 내가 감당할 수 없는 상황이 일어날까봐 써볼 엄두가 안났다. 다음에 뭔가를 만들땐 좀 더 공부해서 써봐야겠다는 생각이 들었다.
작업기간은 약 이틀정도 소요됐다.