버튼 하나도 생각보다 복잡하다.
사실 버튼 하나는 단순하다고 생각했다. 버튼을 누르면 타이머가 정지하는 정도라 여겼다. 그런데 리액트로 구현을 하다보니 버튼 하나가 일으키는 파장은 엄청난것 같다. 컴포넌트간 스테이트가 얽혀있어 예상하기가 너무 힘들다. 버튼하나를 눌렀는데 다른 버튼이 갑자기 눌려서 타이머가 멈추기도 했다. 제어하기 까다롭지만 매력적이라는 생각이 든다. 다른 동기들에게 듣기로는 리덕스라는 포맷?이 있다고 한다. 루트에서 스테이트를 관리해 매번 props로 전달할 필요가 없지만 구현이 까다롭다고 한다. 나중에 시간이 허락한다면 리덕스를 공부해 보는 것도 나쁘지 않은 것 같다.
문제상황에 대한 데이터가 필요하다.
처음 타이머와 AI 모델이 병합되어 급격한 성능저하가 발생했을 때 팀원들과 원인을 찾아보기 시작했다. 그런데 되돌아 생각해보니 원인을 추측하는 것이지 분석하지 않았다. 크롬의 개발자 도구만 활용했어도 원인 분석에 도움이 될 데이터가 많았을 것 같은데 데이터를 쌓을 생각을 못했다. 그냥 어떻게든 해결하고 싶은 마음만 앞선것 같았다. 현업에서의 문제는 고객과 밀접한 관련이 있기 때문에 문제를 훨씬 급하게 처리해야 할 것이다. 이런 경험이 없었다면 시간의 압박때문에 제대로된 원인 분석을 하지 않고 초조하게 추측만 했을 것 같다. 남은 2주간 문제는 또 나타날 것이다. 문제의 원인을 제대로 찾는데 도움이 될 만한 데이터를 모으는 것에서 부터 시작해야 할 것 같다.
일단 돌아가게 만들자.
DB 설계를 하며 팀원들과 어떤 방식으로 구현 할 것인지 고민했다. 공부 시간을 기록하기 위해서 타이머 종료 시 공부 시작시간과 종료시간을 DB에 전달하면 그만이라 생각했다. 하지만 다른 팀원이 만약 타이머를 시작했는데 새로고침을 하거나 인터넷이 끊기면 정보를 DB에 전달 할 수 없다는 의견을 제시했다. 이런 경우 내가 4시간을 공부해도 통계 자료에 반영이 되지 않는 것이다.
이 문제를 해결하기 위해서는 인터넷이 끊기거나 브라우저가 닫힐 때 자동으로 서버에 요청해야한다. 그런데 인터넷이 끊기고 브라우저가 닫히면 서버에 요청하는 것이 불가능 하다. 따라서 일정 주기로 서버에 요청하여 종료시간을 업데이트 하는 방법이 있다. 하지만 일정시간 서버에 종료 시간 수정을 요청하면 일정 주기로 DB에 접근해야한다. 이는 비용 효율면에서 불리할 수 있다.
하지만 생각해보니 회사 비용을 아끼기 위해 고객의 신뢰를 버리는 것은 리스크가 너무 컸다. 게다가 구현도 너무 막막했다. 팀원들과 논의한 결과는 타이머 정지시 공부 시작시간과 종료시간만 전달하기로 했다. 우선 서비스가 돌아가게 만들고 나중에 개선하기로 하였다. → 이런 결정을 내린 후 이서기 멘토님과 미팅을 했다. 해당 고민을 말씀 드렸는데 DB는 생각보다 강하기 때문에 여러번 DB에 가도 무관할 거라 말씀하셨다. 역시 고민도 데이터를 바탕으로 해야하는 것 같다.
프로그램 개발은 속도가 생명이라 생각한다. 너무 완벽하게 만들려 하면 경쟁자가 이미 만들어버린다. 그래서 구현하고자 하는 기능의 범위를 정확하게 정하고 해당 방향을 밀고 나가야 하는것 같다. 물론 내 실력을 키워서 빠르게 개발하는 것이 기본이라 생각한다.
이런 생각을 한 후에 일일 공부 시간을 타임 히트맵으로 보여주는 기능을 구현하였다. 처음에는 과목 1개만의 공부시간을 보여줄 수 있는 타임 히트맵을 빠르게 구현하고 나중에 전체 과목의 히트맵을 구현 할 수 있는 기능을 추가했다. 아직 기능이 완벽하지는 않지만 전보다 빠르게 기능을 구현했다고 생각한다. 최소 기능을 정확하게 정하고 계속 발전시켜 나가는 방향으로 진행해야 할 것 같다.

import style from "./TimeHeatmap.module.css";
import {HeatMapGrid} from "react-grid-heatmap";
import { useState } from "react";
function hexToRgb(hex) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
let shorthandRegex = /^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
let result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function TimeHeatmap ({data , labels , subjectColors}){
const [subject , setSubject] = useState(labels[0]);
const xLabels = new Array(6).fill(0).map((_, i) => `${i}`);
const yLabels = new Array(25).fill(0).map((_, i) => `${i}h`.padStart(3,0));
const subjectTotalTime = data.subjectTotalTime;
let colorRGB;
subjectTotalTime.map((e)=>{
if(e.subjectName === subject){
colorRGB = e.color;
}
});
const color = hexToRgb(colorRGB);
const getData = data.rangeTime;
const inputData = new Array(25).fill(0).map(()=>
new Array(6).fill(0)
)
getData.map((value)=>{
const subjectName = value.subjectName;
if(subjectName === subject){
const startH = parseInt(value.startTime.slice(11,13));
const startM = parseInt(value.startTime.slice(14,16));
const startX = parseInt(startM / 10);
const endH = parseInt(value.endTime.slice(11,13));
const endM = parseInt(value.endTime.slice(14,16));
const endX = parseInt(endM / 10);
if(startH === endH){
inputData[startH][startX] = startM - startX * 10;
inputData[startH][endX] = endM - endX * 10;
for(let i = startX + 1 ; i < endX ; i++){
inputData[startH][i] = 10;
}
}
else{
inputData[startH][startX] = startM - startX * 10;
for(let i = startX + 1 ; i < 6 ; i++){
inputData[startH][i] = 10;
}
for(let y = startH+1 ; y < endH ; y++){
for(let x = 0 ; x < 6 ; x++){
inputData[y][x] = 10;
}
}
for(let j = 0 ; j < endX; j++){
inputData[endH][j] = 10;
}
inputData[endH][endX] = endM - endX * 10;
}
}
})
console.log(inputData);
const handleSelect = (e) => {
setSubject(e.target.value);
}
return(
<div className={style.heatmap}>
<select onChange={handleSelect}>
{labels.map((label,i) => <option key={`${i}`} value={label}>{label}</option>)}
</select>
<HeatMapGrid
data={inputData}
xLabels={xLabels}
yLabels={yLabels}
xLabelsStyle={(index) => ({
display : 'none',
})}
yLabelsStyle={() => ({
fontSize: '.7rem',
color: '#777'
})}
cellStyle={(_x, _y, ratio) => ({
background: `rgb(${color.r}, ${color.g}, ${color.b}, ${ratio*0.8})`,
border : `${ratio === 0 ? "solid 0.5px" : ""}`,
borderRadius : 0,
borderColor : `rgb(${color.r}, ${color.g}, ${color.b}, 0.1)`
})}
cellHeight="1.5rem"
square
/>
</div>
);
}
export default TimeHeatmap;
import style from "./TimeHeatmap.module.css";
import {HeatMapGrid} from "react-grid-heatmap";
import { useState } from "react";
function hexToRgb(hex) {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
let shorthandRegex = /^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
let result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function TimeHeatmap ({data , labels , subjectColors}){
const [subject , setSubject] = useState("전체");
const xLabels = new Array(6).fill(0).map((_, i) => `${i}`);
const yLabels = new Array(25).fill(0).map((_, i) => `${i}h`.padStart(3,0));
const subjectTotalTime = data.subjectTotalTime;
const colorRGB = {};
const totalInputData = {};
const timeColorYX = {};
const getData = data.rangeTime;
subjectTotalTime.map((e)=>{
colorRGB[e.subjectName] = hexToRgb(e.color);
});
const inputData = new Array(25).fill(0).map(()=>
new Array(6).fill(0)
)
getData.map((value)=>{
const subjectName = value.subjectName;
const startH = parseInt(value.startTime.slice(11,13));
const startM = parseInt(value.startTime.slice(14,16));
const startX = parseInt(startM / 10);
const endH = parseInt(value.endTime.slice(11,13));
const endM = parseInt(value.endTime.slice(14,16));
const endX = parseInt(endM / 10);
if(startH === endH){
if(inputData[startH][startX] < startM - startX * 10){
inputData[startH][startX] = startM - startX * 10;
timeColorYX[String(startH)+String(startX)] = subjectName;
}
if(inputData[endH][endX] < endM - endX * 10){
inputData[endH][endX] = endM - endX * 10;
timeColorYX[String(endH)+String(endX)] = subjectName;
}
for(let i = startX + 1 ; i < endX ; i++){
inputData[startH][i] = 10;
timeColorYX[String(startH)+String(i)] = subjectName;
}
}
else{
if(inputData[startH][startX] < startM - startX * 10){
inputData[startH][startX] = startM - startX * 10;
timeColorYX[String(startH)+String(startX)] = subjectName;
}
for(let i = startX + 1 ; i < 6 ; i++){
inputData[startH][i] = 10;
timeColorYX[String(startH)+String(i)] = subjectName;
}
for(let y = startH+1 ; y < endH ; y++){
for(let x = 0 ; x < 6 ; x++){
inputData[y][x] = 10;
timeColorYX[String(y)+String(x)] = subjectName;
}
}
for(let j = 0 ; j < endX; j++){
inputData[endH][j] = 10;
timeColorYX[String(endH)+String(j)] = subjectName;
}
if(inputData[endH][endX] < endM - endX * 10){
inputData[endH][endX] = endM - endX * 10;
timeColorYX[String(endH)+String(endX)] = subjectName;
}
}
})
const handleSelect = (e) => {
setSubject(e.target.value);
}
console.log(inputData);
console.log(colorRGB);
console.log(timeColorYX);
console.log(subject);
return(
<div className={style.heatmap}>
<select onChange={handleSelect}>
<option key="total" value="전체">전체</option>
{labels.map((label,i) => <option key={`${i}`} value={label}>{label}</option>)}
</select>
<HeatMapGrid
data={inputData}
xLabels={xLabels}
yLabels={yLabels}
xLabelsStyle={(index) => ({
display : 'none',
})}
yLabelsStyle={() => ({
fontSize: '.7rem',
color: '#777'
})}
// cellRender={(y, x, value) => (
// <div title={`Pos(${x}, ${y}) = ${value}`}>{String(y)+String(x)}</div>
// )}
cellStyle={(_y, _x, ratio) => (
{
background: `${
timeColorYX[String(_y)+String(_x)] === subject || (subject === "전체" && timeColorYX[String(_y)+String(_x)] !== undefined) ?
// "rgb("+colorRGB[subject].r+","+colorRGB[subject].g+","+colorRGB[subject].b+","+ratio+")"
"rgb("+colorRGB[timeColorYX[String(_y)+String(_x)]].r+","+colorRGB[timeColorYX[String(_y)+String(_x)]].g+","+colorRGB[timeColorYX[String(_y)+String(_x)]].b+","+ratio+")"
:""}`,
border : `${ratio === 0 ? "solid 0.1px": "solid 0.05px"}`,
borderRadius : 0,
// borderColor : `rgb(${color.r}, ${color.g}, ${color.b}, 0.1)`
})}
cellHeight="1.5rem"
square
/>
</div>
);
}
export default TimeHeatmap;
더 공부해야 할 것