메인프로젝트가 끝나고 리팩토링과 함께 자바스크립트DeepDive 책을 다시 정독하면서 쉬엄쉬엄 뭔가를 만들어보고 싶은 마음에 뭔가 무겁지않고 새로운걸 만들어보려하다가 만들어보게 되었습니다.
Lofi-App
프로젝트 구상 계기
뭔가 흔하다면 흔한? 음악관련 웹앱을 만들어본적이 없었습니다. 한번쯤 만들어보고싶었던 상황에서 요즘 코드치면서 많이 들었던 lofi음악에 관하여 만들어보고싶다는 생각이 들어서 만들어보게 되었습니다.
사용 주요스택
- React
- TypeScript
- Mui
- Redux-toolkit
- sass
- react-timer-hook
- react-player
- react-audio-player
먼저, 타입스크립트는 아직 많이 부족하기에 거의 무조건 사용한다고 생각을하였었고, 음악을 재생하기 위해 검색을해보다가 audio태그와 react-audio-player라이브러리를 활용하여 재생을 시킬 수 있다는 것을 알게 되었고, 해당 내용에 대해 사이트들을 참고하여 사용을 하게되었다.
참고2 https://www.npmjs.com/package/react-audio-player
또한 다크모드 설정, todo, mode, mood 등은 상태를 만들어 관리를 해주어야하였기 때문에 상태관리 라이브러리로 redux-toolkit을 사용하기로 정하였다.
스타일 라이브러리로는 sass를 선택하였는데 그 이유는 몇번 sass를 사용해본적이 있었는데 css에 비해 여러가지 장점들이 있지만, 개인적으로 가장 크게 느껴지는 장점은 nesting이 가능하다는 것이다. 즉, 중첩해서 선언과 사용이 가능하다는 것이다. 예를들어,
.container {
width : 100%;
}
.container h1 {
color : red;
}
원래의 css에 자식요소에 스타일을 주기 위해서는 위의 예시처럼 작성하여야했다. 하지만 scss를 사용하면
.container {
width : 100%;
h1 {
color : red;
}
}
위처럼 사용이 가능하다. 별 차이가 없어보이지만, 스타일 코드들이 많아지면 가시성이 꽤나 많이 좋아진다.
또한 투두리스트와 함께 focus mode로 들어갈 timer도 라이브러리를 이용하여 만들었다.
참고3 https://www.npmjs.com/package/react-timer-hook
위의 npm 페이지에서 해당 라이브러리 사용법을 참고하였다.
프로젝트 회고
겪은 시행착오
프로젝트 시작할 때부터 DeepDive공부와 메인프로젝트 리팩토링을 함께 진행할 생각이었기 때문에 기능상으로는 크게 어려운점은 없었다. 하지만 여전히 타입에러는 골치가 너무아팠다. 만났던 에러들을 다시 복기하면서 다음에 또 있을 에러들에 대비하여보자!
1. mp3파일들의 위치
음악들을 mp3확장자로 받아와서 public 폴더의 assets에 넣을 것인지, 모듈로 간주하여 src폴더 내부에서 관리할 것인지 정해야했다. 저는 public폴더에 직접 넣어서 웹팩 등의 빌드 과정에서 별도의 처리없이 사용을 하였고, '/노래제목.mp3' 이런식으로 파일에 접근을 하였다. 하지만, 이러한 방법에 단점도 존재하였다. 빌드할 때 파일크기 최적화나 경로 변경이 어려울 수 있고, 파일명 충돌이나 관리 어려움이 발생할 수 있는 단점이 있었다.
2. Invalid type "false | "active"" of template literal expression
// 해결 전
<div className="modifier__icon">
<div className={`icon ${moodOpen && "active"}`}></div>
</div>;
템플릿 리터럴 표현식으로 className을 동적으로 설정하기 위해서 사용하였는데 Invalid type "false | "active"" of template literal expression. 에러가 발생하였다. 해당 에러는 표현식의 결과가 유효하지 않아서 발생한 에러였다. 해결 방법은 너무나 간단했다.
// 해결 후
<div className="modifier__icon">
<div className={`icon ${moodOpen ? "active" : ""}`}></div>
</div>;
위의 코드로 수정하여 삼항연산자로 moodOpen이 true일때만 active를 반환하고, false일때는 빈문자열("")을 반환하도록 처리해주었다. 생각해보면 너무 멍청한 코드였다. moodOpen가 true일때 "active"를 넣어준다는 생각만 했기 때문에 말도안되는 코드를 작성하였던 것이다.
3. ~코드에서 이 호출과 일치하는 오버로드가 없습니다.
위의 에러가 발생하는 주요 이유는
- 호출하는 인자의 타입이 일치하는 오버로드 시그니처가 없을 때
function processValue(value: string): void;
function processValue(value: number): void;
const inputValue: boolean = true;
processValue(inputValue); // 에러: 일치하는 오버로드 없음
- 인자 개수가 일치하는 오버로드 시그니처가 없을 때
function greet(name: string): void;
function greet(name: string, age: number): void;
greet("Alice", 25); // 에러: 일치하는 오버로드 없음
이렇게 두 이유가 가장 주요하다고 합니다.
첫번째의 경우는 inputValue의 타입이 boolean이므로 processValue함수에 맞는 오버로드 시그니처가 없기 때문에 에러가 발생하기 때문에 processValue함수에 value:boolean으로 올바른 오버로드 시그니처를 추가하거나, 타입 체크를 수정하는 것으로 해결이 가능하다.
두번째의 경우는 함수 호출 부분에서 오버로드된 함수를 호출할 때 모호성이 생기기 때문에 타입에러가 발생하여 호출 시에 매개변수를 명시적으로 구분하여 오버로드를 선택하도록 하는것으로 해결이 가능하다.
4. 타이머에서 에러도 뜨지않고 hour, minute, second 값도 변경은 되는데 렌더링되는 시, 분, 초가 다 0으로 되어있으면서 실행이 안됨
// 해결 전
const setTimeHandler = ({ hour, minute, second }: timeType) => {
const time = new Date();
const setupTime =
Number(hour) * 3600 + Number(minute) * 60 + Number(second);
time.setSeconds(time.getSeconds() + setupTime);
};
useTime 훅을 잘못 사용하고 있는 것 같아 계속해서 npm문서를 찾아보았다. 그러던 중 https://github.com/amrlabib/react-timer-hook 해당 사이트에서 해답을 찾을 수 있었다. 시간을 설정해서 타이머를 돌리는 setTimeHandler 함수에서 restart메서드를 넣어주지 않아서 0시간 0분 0초로 계속 머물러있었던 것이다. restart(time)메서드를 넣어줌으로써 default값으로 되어있는 true값을 통해 사용자가 설정한 hour, minute, second로 다시 시작이 되어야 하는 것이였다.
// 해결 후
const setTimeHandler = ({ hour, minute, second }: timeType) => {
const time = new Date();
const setupTime =
Number(hour) * 3600 + Number(minute) * 60 + Number(second);
time.setSeconds(time.getSeconds() + setupTime);
restart(time);
};
5. 다크모드 설정 중 타입에러
// 해결 전
interface ThemeProp{
theme : string;
}
const MaterialUISwitch = styled(Switch)(({ theme }:ThemeProp)=>({
...
))}
다크모드 설정 중에 MaterialUISwitch의 매개변수인 theme에 'day', 'night'가 들어갈 string타입을 지정해주어야하는데, mui에서 가져다쓰는 Theme과 따로 지정해준 'day', 'night'의 타입이 string인 theme과 충돌이 났다. 위의 코드처럼 string으로 줘도 안되고, Theme으로 줘도 안되서 interface를 두개로 나눠서 mui에서 Theme을 가져다쓰는 곳에는 ThemeProp, 따로 지정해준 'day', 'night'를 사용하는 곳에는 stringProp을 주어서 타입에러를 해결하였다.
// 해결 후
interface ThemeProp {
theme: string | Theme | (string & Theme);
}
interface stringProp {
theme: string;
}
const MaterialUISwitch = styled(Switch)(({ theme }: stringProp) => ({
...
))}
아하모먼트
1. e.target.id 로 이벤트 핸들러 함수 내에서 이벤트 객체를 통해 접근할 수 있는 프로퍼티가 있었다.
사실 e.target.value는 많이 사용해봤었는데 e.target.id로 이벤트가 발생한 요소의 id값을 가져올 수 있다는 것을 처음 알았다.
lofi의 mood로 Chill, Jazzy, Sleep을 서로 변경하기 위해서 클릭이벤트를 주는 요소의 id값으로 각각 주었다.
따라서 각각 id값을 가져오기 위해
const moodChangeHandler = (
e: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
const clickedElementId = (e.target as HTMLDivElement).id;
dispatch(changeMoodStatus(clickedElementId));
};
위와 같이 클릭이벤트를 주는 요소의 id인 Chill or Jazzy or Sleep을 가져올 수 있었다.
하지만 아쉬운 점은 리액트 훅인 useRef를 사용하여 current.id로 가져와서 관리를 했었다면 더 좋지않았을까 생각을 한다. 타입도 수정해야 하기에 리팩토링 과정에서 진행해보려한다.
2. Board컴포넌트 작업 중 너무 많아지는 useState 관리
const [cityTraffic, setCityTraffic] = useState<number | undefined>(0);
const [cityRain, setCityRain] = useState(rainValue);
const [fireplace, setFireplace] = useState(0);
const [snow, setSnow] = useState(0);
const [summerStorm, setSummerStorm] = useState(0);
const [fan, setFan] = useState(0);
const [forestNight, setForestNight] = useState(0);
const [wave, setWave] = useState(0);
const [wind, setWind] = useState(0);
const [people, setPeople] = useState(0);
const [river, setRiver] = useState(0);
const [rainForest, setRainForest] = useState(0);
background-sound를 주기 위해서 소리들을 상태로 주고 관리를 하여야하는 상황이였다. 하지만 너무 보기 불편했고 생각보다 무겁다고 메인프로젝트에서 멘토님의 말씀이 기억에 남아서 useState를 적게 사용하고 싶었다. 그렇게해서 해결방법을 찾아보다가 객체를 사용하여 여러 상태를 하나의 상태로 묶어서 깔끔하고 유지보수가 용이하도록 할 수 있었다. 상태 객체 'soundSettings'를 정의해주고, initialSoundSettings 상수를 사용하여 초기값을 설정 updateSoundSettings함수를 만들어 사용하여 하나의 상태 객체로 관리를 할 수 있었다.
const initialSoundSettings: SoundSettings = {
cityTraffic: 0,
cityRain: rainValue,
fireplace: 0,
snow: 0,
summerStorm: 0,
fan: 0,
forestNight: 0,
wave: 0,
wind: 0,
people: 0,
river: 0,
rainForest: 0,
};
const [soundSettings, setSoundSettings] =
useState<SoundSettings>(initialSoundSettings);
const updateSoundSettings = (updates: Partial<SoundSettings>) => {
setSoundSettings((prevSettings) => ({ ...prevSettings, ...updates }));
};
느낀점
위에서 말했다시피 자바스크립트DeepDive, 메인프로젝트 리팩토링을 진행하는 중간중간에 머리를 식히기 위해(?) 진행한 간단한 프로젝트였지만 react-timer-hook, react-player 등 새로운 라이브러리를 접해보아서 아주 유익한 기간이였고, 무엇보다도 타입스크립트에서 자주 봐왔던 에러들을 다시 마주하면서 확실하게 정리하고 넘어갔다는게 너무 좋았다. 또한, netlify를 통해 배포도 해보았고, build를 하는 과정에서 마주쳤던 수많은 에러들... 잡느라 너무 힘들었지만 그만큼 나의 코드가 더 깨끗해지고 군더더기 없어진다~ 생각하고 진행하였다. 타입스크립트도 점점 뭔가 익숙해져 가고있는 느낌이 들고, 아직 타입에러 잡기에도 허덕이고 있지만 하나하나씩 타입스크립트를 왜 사용하는지 알아질 것 같다. 하지만 자바스크립트 기본 문법이 너무 부족하다는 것을 뼈저리게 느끼고 있다. 당장 삼항연산자, 조건부렌더링 등에 대해서도 이해를 하고있는게 아니라고 느껴졌다. 이제 위에서 말했던대로 e.target.id를 useRef로 리팩토링을 하고 타입정의해놓은 것도 정리하는 리팩토링 시간을 가지고, 자바스크립트 DeepDive... 진짜 열심히 보자.