테마 변경을 하는 컴포넌트는 의미상 <button>이 적절하다. 접근성을 고려하여 aria-label과 aria-live를 추가하고, title은 툴팁을 위해 추가한다.
2.2. 아이콘 만들기
아이콘을 위해서는 svg 컴포넌트를 사용할 것이다. 이런 식으로 만들고자 한다.
이를 위해서 지금 작업중인 components/molecules/themeSwitch폴더에서 icon.tsx를 만들고 SunAndMoonIcon컴포넌트를 만들자.
위와 같이 하려면 먼저 중앙에 원을 만들고 주변에 햇살을 표시하는 선들을 그린다. 선들은 그룹핑을 위한 <g>로 묶는다. 그리고 다크 모드의 달 모양을 만들기 위해 원의 일부를 가리는 것은 <mask>로 처리한다. 이렇게 만든 SunAndMoonIcon컴포넌트는 다음과 같이 생겼다.
<svg>에 aria-hidden속성이 적용되어 있는데 이는 스크린 리더가 이를 무시하게 한다. 아이콘은 그냥 시각적인 장식이니까 그렇다. 그리고 <g>의 stroke속성은 선 색깔이다.
2.3. 간단한 동작
이를 시험하기 위해 버튼을 클릭하면 테마가 바뀌는 동작을 넣어보자.
ThemeSwitch컴포넌트를 다음과 같이 수정한다. 이러면 버튼을 클릭할 시 테마가 바뀌는 것을 볼 수 있다(아이콘은 아직 바뀌지 않는다).
참고로 getThemeName은 그냥 테마 이름을 반환하는 간단한 함수이다.
2.4. CSS
해당 가이드를 따라서 아이콘의 CSS를 조작해보자. theme.module.css를 수정하면 된다.
먼저 버튼의 클래스인 .themeToggle을 설정하자. 버튼의 기본 스타일을 없애고 적절히 디자인한다. 또한 outline-offset을 통해 어느 정도의 간격을 준다.
이때 터치스크린을 사용하는 유저에게는 사이즈 2rem이 충분하지 못할 수 있기 때문에 @media (hover: none)을 통해 터치스크린에서는 사이즈를 더 크게 만들어 준다.
또 색을 위한 변수를 설정하는데 reset.css에서 정의해 둔 CSS 변수를 사용한다. reset.css의 색상 변수는 현재 테마에 따라 자동으로 색상이 바뀌도록 되어 있으므로 이렇게 하면 테마에 따라 아이콘의 색을 따로 설정해 줄 필요가 없어진다.
버튼 내부의 svg 컴포넌트의 크기를 조정하고 stroke-linecap으로 선의 양쪽 끝 모양을 둥글게 만들어준다.
그리고 다음과 같이 색상과 선 굵기처럼 자잘한 부분들을 설정하고 애니메이션을 위한 transform속성들을 설정한다.
이렇게 하면 버튼 클릭 시 테마는 물론 아이콘까지 변경되게 된다.
2.5. 애니메이션
자연스러운 아이콘 전환을 위한 애니메이션도 삽입해 보자. 그런데 애니메이션은 저사양 기기에 큰 부하를 걸 수 있고 시각적인 피로 등으로 인해 애니메이션을 보고 싶어하지 않는 설정을 한 사용자도 있을 수 있다.
이런 이유로 애니메이션은 prefers-reduced-motion: no-preference 미디어 쿼리 내부에 존재하도록 한다. 애니메이션 진행은 크롬 개발자 도구의 애니메이션 패널에서 볼 수도 있다. 진행 그래프까지도 보여준다.
어쨌건 postCSS로 작성되어 있는 원 글의 CSS를 풀어서 작성해 보면 다음과 같이 나온다. @nest대용으로 :has를 쓸 수도 있겠지만 아직은 지원되지 않는 브라우저가 꽤 있기 때문에 쓰지 않았다.
버튼 클릭 시 자연스럽게 아이콘이 전환되는 애니메이션까지 보이게 되었다.
3. 새로운 테마 만들기
3.1. 테마 색상 만들기
나는 vscode에서 Light Pink Theme을 사용하고 있고 이 테마를 매우 좋아한다. 따라서 라이트 테마와 다크 테마에 더해 핑크 테마도 만들어 주도록 하자. 앞서 Provider컴포넌트를 만들 때 pink, darkPink 테마에 대한 부분도 미리 만들어 놓았다.
위에서 보았던 ThemeProvider의 value와 themes props를 보면 알 수 있다.
이미 거의 모든 요소들의 색상을 CSS 변수명 기반으로 작성해 놓았으므로 테마들의 색을 src/styles/theme.css에 작성해 놓으면 자동으로 적용된다.
지금 적용할 수 있는 테마는 light, dark의 2종류이다. 그런데 여기 pink와 darkPink를 적용하기 위한 버튼을 하나 더 만들 것이다. 그 동작은 다음과 같이 생각할 수 있다.
현재 있는 해/달 버튼의 경우 현재 상태를 나타내며 누를 시 다른 테마로 이동한다. light 상태이면 dark로, dark 상태이면 light로 간다. 즉 다음과 같이 동작하게 될 것이다.
현재 테마
해/달 버튼 클릭시
light
dark
dark
light
그러면 핑크 테마 전환 버튼은 어떻게 동작해야 할까? 핑크 테마의 존재가 그렇게 익숙하지 않은 것을 생각했을 때 현재 테마에 기반한 테마로 전환해 주는 것이 적절하다고 본다. 따라서 다음과 같은 동작을 생각해볼 수 있다.
또한 핑크 테마에 있는 상태에서 핑크 토글 버튼을 또 누르면 pink <-> darkPink 테마의 전환이 이루어지는 게 적절할 것 같다. 여기까지를 표로 나타내면 다음과 같다.
현재 테마
해/달 버튼 클릭
핑크 버튼 클릭
light
dark
pink
dark
light
darkPink
pink
?
darkPink
darkPink
?
pink
그런데 핑크 테마에서 해/달 버튼을 다시 누르면 무슨 일이 일어나야 하는가? light/dark 테마에서 핑크 버튼을 클릭했을 때 일어났던 일을 생각해볼 때 반대의 경우도 기대할 수 있을 거라고 생각했다. light pink 테마에서 해 버튼을 눌렀는데 갑자기 dark 테마로 가는 것을 기대하지는 않을 거라고 보았다. 따라서 다음과 같이 테마 토글 버튼들이 동작하도록 설계해 보았다.
현재 테마
해/달 버튼 클릭
핑크 버튼 클릭
light
dark
pink
dark
light
darkPink
pink
light
darkPink
darkPink
dark
pink
3.3. 기존 버튼을 컴포넌트로 분리
슬슬 버튼들을 그냥 컴포넌트로 분리해 주는 게 나을 것 같다. 따라서 themeSwitch폴더 내에 lightDarkToggle과 pinkToggle 폴더를 만들고 각각에 index.tsx와 styles.module.css를 만들어준다. LightDarkToggle컴포넌트는 다음과 같이 만들어진다.
그리고 lightDarkToggle/styles.module.css에는 위에서 정의한 CSS 중 해/달 버튼에 해당되는 부분을 복사해서 넣어주자.
3.4. 핑크 버튼 마크업
핑크 버튼도 다음과 같이 만들어줄 수 있다. 위에서와 비슷하게 마크업을 잡고 적당한 svg 아이콘을 넣어주면 된다. 나는 light/dark 토글 버튼이 해/달로 된 것에 착안해서 별 svg 아이콘을 찾아서 넣어주었다.
3.5. 테마 토글 버튼 동작
핑크 테마 버튼을 스타일링하기 전에 먼저 위에서 설계했던 동작을 구현하자. themeSwitch/index.tsx에서 적당한 toggleClick함수들을 정의해 주고 위에서 만든 컴포넌트들에 props로 넘겨주면 된다. isDarkOrPink변수를 만들어서 조정했다.
3.6. 핑크 테마 버튼 디자인
CSS는 다음과 같이 디자인했다. .pinkThemeToggle은 LightDarkToggle을 디자인할 때와 비슷하고 적절한 색을 찾아 준 정도다. 그리고 버튼 호버 시 마치 별이 떨어지는 것과 같은 애니메이션을 주도록 했다.
[data-theme='dark']와 [data-theme='darkPink']는 :is 유사 클래스를 사용하거나 좀더 오래된 방식으로는 [data-theme^='dark'](dark로 시작하는 data-theme 속성을 모두 선택) 선택자를 사용해서 더 짧게 둘 다 선택할 수 있다.
하지만 next-themes에서 이를 알아서 해주기 때문에 여기서 할 필요는 없다. 만약 필요한 사람이 이 글을 본다면, 원문 링크를 참고하자.
5. 추가 수정 작업
(2023-09-22 작성)
5.1. 테마 토글 버튼 디자인 수정
버튼에 hover하면 애니메이션이 나오는 게 괜찮은 것 같아서 애니메이션을 넣었었다. LightDarkToggle의 경우 호버시 약간 회전하는 효과를, PinkToggle의 경우 별이 떨어지는 효과를 넣었다.
그런데 모바일에서는 한번 클릭시 다른 곳을 클릭할 때까지 호버 상태가 유지되기 때문에 이상해 보인다. 따라서 이를 PC에서만 보이도록 수정하자.
호버가 가능하고 정확한 포인터를 쓰는 경우, 그러니까 @media (hover: hover) 그리고 @media (pointer: fine) 미디어 쿼리를 적용하면 된다. 애니메이션이 쓰인 곳에 다음과 같이 nested media query를 적용하자.
LightDarkToggle의 CSS에도 똑같이 적용해 주면 된다.
5.2. 코드 테마 수정
이 블로그는 마크다운 기반으로 글을 작성할 수 있다. 그리고 마크다운을 블로그 글에 필요한 정보들로 변환해 주는 작업은 contentlayer라는 라이브러리가 담당한다. 그러면 코드 블록은 어떻게 하이라이팅될까?
이는 rehype-pretty-code라는 라이브러리가 해주고 있다. contentlayer.config.js파일을 보면 이를 사용하고 있는 걸 확인도 가능하다. 이 설정파일을 좀 만지면 코드 블록의 테마를 바꿀 수 있다. 일단 핑크 테마는 light-plus 테마가 좋아보인다.
해당 json 파일을 가져와서 적당히 포매팅하고 public/themes/dark-pink-themes.json에 저장해 놓았다. 그리고 contentlayer.config.js의 rehypePrettyCodeOptions에 다음과 같이 추가해 주었다. 해당 파일을 가져와서 JSON.parse를 해주면 된다.
5.2.1. 테마 색상 부족 문제
그런데 문제가 하나 생겼다. dark pink theme의 color json을 가져와서 적용했는데 몇몇 부분에서 색이 너무 밋밋하게 나오고 있었다. 예를 들어 현재 pink theme(코드 테마는 light-plus)에서 NextJS metadata 오류 해결글에 있는 내 코드 하나를 보면 이렇게 나오고 있다.
같은 코드를 dark pink theme으로 바꾸면 다음과 같이 나온다.
가장 밋밋하게 나온 코드를 찍은 것이기 때문에 더 낫게 나온 부분들도 있다. 하지만 상대적으로 밋밋하다는 것은 여전하다. 그리고 vscode의 light pink theme에 있는 dark pink 테마는 원래 좀 색이 밋밋하기는 하지만 그것보다도 더 적용되는 색이 적다! 뭔가 제대로 적용되지 못하고 있는 것 같다.
따라서 잘 적용되고 있는 shiki의 light-plus 테마 설정 파일을 가져와서 비교해 보기로 했다. light-plus 테마 파일이 약 200줄 더 긴 걸 보면 뭔가 더 있기는 있는 것 같다.
거기서 가르쳐주는 차이들을 light plus 테마들로 고치자. 예를 들어서 $schema 프로퍼티를 추가하는 것과 같이. 그러면 editorIndentGuide.background는 deprecated 되었고 editorIndentGuide.background1를 사용하라는 등 몇 가지 warning을 띄워준다.
이런 프로퍼티명을 고치는 것들은 금방 했지만 그 외의 대부분의 것들은 노가다였다. JSON 파일 차이를 보고 tokenColors에 정의되어 있는 색상들을 하나하나 찾아 주는 그런 일들.
예를 들어서 이런 방식으로 진행되었다. light plus 테마 설정 파일의 tokenColors배열에는 다음과 같은 프로퍼티가 있고 dark pink 테마 설정 파일에는 없다.
그럼 이 #795E26이라는 색상은 어디에 쓰이고 있는가? semanticTokenColors.customLiteral이 바로 이 색상이다. 그리고 이 속성은 dark pink 테마 설정 파일에도 #d4d4d4로 정의되어 있다. 그러면 dark pink 테마 설정 파일에는 다음과 같이 추가해 주면 되는 것이다.
없는 색상은 적당히 추가해 가면서 테마의 색상을 보강하였다. 현재 Dark pink 테마는 분홍, 보라, 연한 파랑을 중심으로 색상들이 입혀져 있는 것을 참고하였다.
이런 쪽의 전문은 아니기 때문에 품질을 장담할 수는 없지만 가독성을 떨어뜨리는 색상은 최대한 배제하고 각 토큰간의 구별이 최대한 잘 되도록 색상을 입혀 보았다.
파랑색 계열은 분홍색 계열로, 빨강 계열 색은 하늘색으로, 그 외 색은 적절한 색을 다른 테마나 팔레트 등에서 가져와서 입혔다. 각 색의 밝기 조절에 대해서는 Open Color사이트를 참고하였다.
그 외에도 다른 메이저 테마들의 설정파일을 몇 참고하였다.
그렇게 설정하고 다시 빌드해 보니 이제 코드 블록의 색상이 더 잘 입혀진 것을 확인할 수 있었다. 물론 디자인이 마음에 들지는 않지만 내가 디자이너는 아니라서 이런 곳에 시간을 많이 쓰지는 않았다...