SUZ!E

this.music

어떤 음악을 듣고 그 음악을 들었던 때로 돌아가는 듯한 느낌을 받은 적이 있나요?
어, 이 음악! 하며 그때로 돌아가게 되죠.
“이 음악”을 언제 들었는지, 음악을 들으며 했던 생각들을 기록하고
언제든 그 음악을 들었던 때를 떠올려봐요.

Go to demo & Github repo

Design

Color

  • HEX #2000FF
  • RGB (32, 0, 255)
  • CMYK (87,100,0,0)
  • HEX #DEB6B6
  • RGB (222, 182, 182)
  • CMYK (0,18,18,13)
  • HEX #DED0D0
  • RGB(222, 208, 208)
  • CMYK (0,6,6,13)
  • HEX #E9E3E2
  • RGB (233, 227, 226)
  • CMYK(0,3,3,9)
  • HEX #121212
  • RGB (18, 18, 18)
  • CMYK (0,0,0,93)

UI/UX

사용자가 가장 처음 접하게 되는 화면입니다. 사용자가 처음 페이지에 접속했을 때, 이 웹앱이 어떤 역할을 하는지 바로 알 수 있도록 직관적인 소개 글과 버튼을 배치했습니다.

버튼을 클릭해 음악을 추가하는 화면으로 넘어왔습니다! 이 페이지에서 우리는 음악을 검색하고, 검색된 음악 목록 중 추가하고 싶은 음악을 선택합니다. 두 번째 단계에서는 이 노래에 지정할 플레이리스트와 일자를 선택합니다. (셀렉트 박스에서 카테고리를 추가할 수 있어요!) 마지막 단계로는, 이 음악에 대한 생각을 기록하고 저장합니다. 그리고 저장 버튼을 누르면...

(좌) 음악추가 화면 • (우) 플레이리스트 셀렉트 박스

Voilà! 우리가 추가한 음악과 플레이리스트 목록이 보이네요. 플레이리스트를 클릭하면 각각의 플레이리스트에 해당하는 음악 카드가 나타납니다.

플레이리스트를 수정하고 싶다거나, 설정했던 날짜를 바꾸고 싶다거나, 이 음악에 대한 새로운 의견을 추가 하고 싶다고요? 그럴 땐 음악 카드를 클릭하면 나타나는 수정 모달에서 수정하면 됩니다! 실시간으로 렌더링 되는 수정된 결과를 확인해보세요.

Responsive Design

지금은 데스크탑 버전만 있지만, 버전 2에서는 반응형 디자인을 적용할 예정이에요! 미리 디자인해둔 this.music의 모바일 버전 모습을 확인해볼까요?

반응형 this.music 커밍쑨!

Development

Development stack

컴포넌트에 props 전달하기

핸들러를 추가해 클릭한 음악 카드의 ID값을 수정 모달에 전달 하고싶어요. props를 넘겨주면 되는데, 타입스크립트에서는 어떻게 할 수 있을까요? 저는 React.FC를 이용해보았어요!

props 타입 정의

            src/pages/Home.tsx
            
import { Playlist } from "@/store/Playlists";

interface MusicProps {
  selectedPlaylist?: Playlist;
  musicId?: string;
  children: ReactNode;
}
            
        

받아온 props를 사용하는 컴포넌트 정의

            src/pages/Home.tsx
            
const MusicLi: React.FC = ({
  musicId,
  selectedPlaylist,
  children,
}) => {
  const { openModal } = useModalStore();

  const handleClick = () => {
    openModal(
      <EditModal
        key="edit-modal"
        musicId={musicId}
        selectedPlaylist={selectedPlaylist}
      />
    );
  };

  return {children};
};
            
        

props를 어디서 받아오는지 확인해볼까요?

            src/pages/Home.tsx
            
import styled from "@emotion/styled";

<MusicCardUl>
  {state.musics &&
    state.musics.map((m, index) => (
      <MusicLi
        key={index}
        musicId={m.id}
        selectedPlaylist={selectedPlaylist}
      >
        // ...content
      </MusicLi>
    ))}
</MusicCardUl>
            
        

서버

일을 할 때에는 서버가 따로 있는 작업을 했는데, 이번에는 제가 서버 담당이죠! 로컬스토리지가 이 프로젝트의 든든한 서버역할을 하고 있으니까요. 추가한 음악 목록을 받아오고 수정하는 일을 서버에서는 어떻게 처리하고 있을지 같이 볼까요?

fetchAll ()

플레이리스트를 인자로 넘겨주면, 그 파라미터의 id 값을 확인해요. this.music의 홈페이지에서 음악을 불러올 때에는 플레이리스트에 따른 음악을 불러와요. 가장 상단에 위치한 “전체 목록” 플레이리스트이 id값은 0이기 때문에 이 경우에는 전체 음악 목록을 반환합니다. 그 외 경우(전체 목록이 아닌 특정 플레이리스트를 클릭했을 경우), 플레이리스트의 id 값과 매치하는 음악 목록만 리턴합니다.

            src/server/musics.ts
            
export const fetchAll = (playlist: Playlist) => {
  const response = localStorage.getItem("musics");
  const musics = response && JSON.parse(response);

  if (playlist.id === "0") {
    return musics;
  } else {
    return musics.filter((m: Music) => m.playlist?.id === playlist.id);
  }
};
            
        

update()

수정은 어떻게 이루어지는지 볼까요? 넘겨받은 id값과 음악 목록의 id값을 비교해 매치하면, 해당 음악의 플레이리스트와 일자와 텍스트를 업데이트해줘요.

            src/server/musics.ts
            
export const update = (
  id: string,
  { playlist, date, text }: { playlist: Playlist; date: string; text: string }
) => {
  const response = localStorage.getItem("musics");
  const musics = response ? JSON.parse(response) : [];

  for (const m of musics) {
    if (m.id === id) {
      m.playlist = playlist;
      m.date = date;
      m.text = text;
    }
  }

  return localStorage.setItem("musics", JSON.stringify(musics));
};
            
        

스토어

이 프로젝트에서는 Context와 useReducer로 스토어를 만들어 사용하고 있어요. 컨텍스트를 생성하고, 어디서든 이 스토어에 접근할 수 있게 하죠. 서버에서 만들어둔 로직을 어떻게 스토어에서 가져와 사용하는지 살펴볼까요?

fetchAll 액션

fetchAll() 액션은 이렇게 서버 로직을 변수에 저장해 dispatch 값으로 넘겨주는 것으로 간단하게 마무리 되었어요!

            src/store/Musics/index.tsx
            
import * as server from "@/server";

const fetchAll = useCallback(
  (playlist) => {
    try {
      const musics = server.fetchAll(playlist);

      dispatch({
        type: ACTION_TYPES.FETCH_ALL,
        musics,
      });
    } catch (e) {
      throw e;
    }
  },
  [dispatch]
);
            
        

Provider

그렇다면 이곳 스토어에서 생성한 Provider는 어떻게 쓰이는지 살펴볼까요?

            src/store/Musics/index.tsx
            
export const Provider = ({ children }: { children: ReactNode }) => {
  const [state, dispatch] = useReducer(reducer, INITIAL_STATE);

  return (
    <Context.Provider value={{ dispatch, state }}>{children}</Context.Provider>
  );
};
            
        

스토어의 루트 디렉토리에서 Provider에 이름을 붙여 export 해줘요.

            src/store/index.tsx
            
export { Provider as MusicsProvider, useMusicsContext } from "./Musics";
            
        

export한 Provider은 이렇게 쓰인답니다. 짠!

            src/App.tsx
            
function App() {
    return (
      <ThemeProvider theme={theme}>
        <LoadingProvider>
          <ModalProvider>
            <MusicsProvider>
              <SearchMusicProvider>
                <PlaylistsProvider>
                  <BrowserRouter>
                    <Router />
                  </BrowserRouter>
                </PlaylistsProvider>
              </SearchMusicProvider>
            </MusicsProvider>
          </ModalProvider>
        </LoadingProvider>
      </ThemeProvider>
    );
  }
            
        

물론 이 파일을 ReactDOM.render의 첫번째 인자로 넘기는걸 깜빡하면 안되겠죠!

            src/index.tsx
            
ReactDOM.render(<App />, document.getElementById("root"));                
            
        

이렇게 정의한 스토어가 뷰 페이지에서는 어떻게 쓰이는지 확인해볼까요? 아주 좋네요!

            src/pages/Home.tsx
            
const { state, ...actions } = useMusicsContext();

const handleClick = useCallback(() => {
  actions.fetchAll(playlist);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [actions.fetchAll]);
            
        

Review

리액트를 사용해 만들어본 첫 번째 웹앱 this-music이다. 어떤 음악을 듣고 나서 “이 음악 아는데! .. 언제 들었더라?” 거나, “이 음악 너무 좋다! 기록해두고 싶어”라는 생각이 들면서 자연스럽게 이런 프로젝트를 진행해봐야겠다는 생각이 들었다.

올해 초 일을 시작하면서 리액트를 처음으로 사용해보게 되었다. 리액트 훅스와 컨텍스트를 함께 사용하며 작업을 해왔으니 일을 하면서 배운 부분을 이 프로젝트에 녹여보고자 노력했다. 그럼에도 아직 수정해야 할 부분이 많다고 생각한다. 지금의 내가 봤을 때 내가 만족할 수 있는 수준의 코드지만 분명 시간이 흐른 후, 또는 누군가가 봤을 때는 고쳐나가야 할 부분이 많은 코드일 수 있다. 어떤 부분이 더 나아질 수 있다는 걸 알게 되는 건 언제나 기쁜 일이다. 타입스크립트나, 훅스에 대해서 더 익숙해지게 되면 분명히 이 프로젝트의 버전 2는 더 멋진 모습을 갖추게 될 거라고 생각한다. 그래서 이어서 버전 2에서 추가하거나 발전시키고 싶은 부분에 대해서 말해보겠다.

앞으로 추가하거나 발전시키고 싶은 부분

지금까지 작업했던 프로젝트 중에는 가장 규모가 큰 (한 페이지를 넘긴!) 프로젝트였다. 지금 와서 보니 그전에 했던 것들이 허접해 보이지만, 또 그런 순간들이 있었기 때문에 지금의 순간도 있다고 생각한다. 무궁무진한 프로그래밍의 세계에 내가 정말로 들어왔다는 생각이 들기도 했고, 그전에는 잘 모르고 썼던 것들에 대한 개념을 제대로 이해하기 시작하면서 느끼는 재미도 있었다. 앞으로도 내가 만들고자 하는 것을 재밌게 만들어보고 싶다.

Live Demo Github Repo