달려가는 차에 도색하기
이 글은 정보 제공일 수도 있고 회고일 수도 있는 글입니다.
험난 했던 3주간의 여정을 마친 채 정신줄을 꽉 잡고 쓰는 글이라 글의 서두가 불안정할 수도 있으니
양해 부탁드립니다. 🙇🏻
이번 마이루틴앱에 없던 다크모드를 맡게 되었습니다 !
작고 소중해보일 수 있는 앱이지만 생각보다 코드가 많고 복잡하지만 추상화와 구조화가 꽤 되어있답니다.
글로벌까지 진출한 앱인데 다크모드가 없다구!?
전반적으로 동일 조명 조건에서 서양인이 동양인보다 공간의 밝기를 더 밝게 지각하는 것으로 나타났다.
라는 결과가 있더라구요 ! 그래서 서양인들이 빛의 민감도가 높아서 그런지 간접 조명도 쓰고 좀 더 어둡게 조명을 하는 것 같습니다.
사실 글로벌 유저들만의 문제가 아니라 CS로 종종 다크모드가 당연히 있는 앱인 줄 알고 있는 유저분들이 많이 계셨습니다. 하지만 팀 내에서 계속 치고나가야하는 feature들이 있거나 이슈가 있는 부분을 처리하다보니 계속 늦어지더라구요.
이젠 정말 해야겠다 라고 판단이 설 때쯤 우선 순위의 온도를 조금 더 올리기 위해 한 말 중
"다크모드는 오늘 하는 게 제일 빠릅니다."
라는 말이 생각이 나는 것 같아요.
왜냐하면 미뤄지다보면 새로운 기능이나 스크린이 추가 되는 경우도 있기 때문에 지금이 가장 빠를 때다! 라는 마음을 항상 갖고 있었습니다.
(사실 디자인 관련 작업엔 항상 진심이라 제가 하고 싶기도 했구요 ㅎㅎ..)
우리... 컬러 시스템 어떻게 쓰고 있지..?
굉장히 걱정 되었습니다. 디자이너 분께서 빠르게 디자인 파일을 전달해주셨고 색 팔레트를 다 뽑아서 전달해주셨거든요. 하지만 어떻게 작업을 해야할지 모르고 먼저 홈하면이나 주로 사용되는 스크린만 작업해서 느낌을 보여드리는게 낫겠지 ?
하며 안일하게 작업을 했습니다.
예를 들어 아래와 같은 코드입니다.
const App: React.FC = () => {
const colorScheme = useColorScheme();
return (
<ThemeProvider theme={colorScheme === 'light' ? light : dark}>
<Provider store={store}>
<ErrorBoundary>
<RootNavigation />
</ErrorBoundary>
</Provider>
</ThemeProvider>
);
};
// 컬러를 받아오는 함수 및 인터페이스
export type Theme = {
// background color
backgroundColor: string;
homeFilterActiveBackgroundColor: string;
bottomTabBarIconActiveBackgroundColor: string;
bottomTabBarIconInactiveBackgroundColor: string;
actionButtonBackgroundColor: string;
homeTrafficLightBackgroundColor: string;
mainTabNavigatorBackgroundColor: string;
homeTabBarBackgroundColor: string;
timeFilterButtonActiveBackgroundColor: string;
timeFilterButtonBackgroundColor: string;
...
}
export const light: Theme = {
// background color
backgroundColor: gray5,
homeFilterActiveBackgroundColor: oliveGreen5,
bottomTabBarIconActiveBackgroundColor: routineGreen60,
bottomTabBarIconInactiveBackgroundColor: gray80,
actionButtonBackgroundColor: routineGreen60,
homeTrafficLightBackgroundColor: white,
mainTabNavigatorBackgroundColor: white,
homeTabBarBackgroundColor: gray5,
timeFilterButtonActiveBackgroundColor: gray70,
timeFilterButtonBackgroundColor: gray10,
...
}
export const dark: Theme = {
// background color
backgroundColor: '#2b2b2b',
homeFilterActiveBackgroundColor: '#505050',
bottomTabBarIconActiveBackgroundColor: gray10,
bottomTabBarIconInactiveBackgroundColor: gray30,
actionButtonBackgroundColor: '#505050',
homeTrafficLightBackgroundColor: '#2b2b2b',
mainTabNavigatorBackgroundColor: '#2b2b2b',
homeTabBarBackgroundColor: '#2b2b2b',
timeFilterButtonActiveBackgroundColor: '#505050',
timeFilterButtonBackgroundColor: white,
...
}
코드 전부를 보여드릴 순 없지만 느낌이 오실겁니다.
컴포넌트에서 사용하는 걸 하나하나씩 다... 적어준다고........?
물론 되게 스마트하고 프래그래머틱하게 진행을 하고 싶었습니다.
왜냐? 개발자니까요
일단 그래도 1차 피드백을 요청 드렸습니다. 꿋꿋히 ^^...
디자이너분 👨🏻🎨 / 본인 🧑🏻💻
👨🏻🎨 : 사실 어느정도 아웃풋이 나왔을 거라 생각했는데 안되어있는 부분도 정말 많았고 색이 다른 부분이 정말 많다. 왜 이런 결과물이 나온거죠?
🧑🏻💻 : 음 먼저 느낌을 보여드리고 싶었어요. 이렇게 진행을 해도 될지?
과거 토스에서 꽤 오랜 기간 재직을 하셨던 분이라 그런지 대화를 주고 나눌 때마다 실시간으로 배움이 생겼습니다.
👨🏻🎨 : 분명 저 컬러값을 더 효율적으로 할 수 있는 방법이 있을 것 같다. 하지만 지금 상태로는 너무 오래 걸릴 것 같다.
🧑🏻💻 : 흠.... (침묵 후 생각 중)
🧑🏻💻 : 일단 알겠습니다. 그럼 저희가 어떻게든 금요일까지 다시 작업한 화면을 보여드릴게요.
👨🏻🎨 : 알겠습니다~ 감사합니다~!
같이 진행한 PM이자 CTO인 민식님과 방법을 고민하던 중 번뜩이는 아이디어가 떠올랐습니다.
그럼 이 컬러 데이터 값을 export 하는 곳에서 디바이스 스키마나 유저가 정한 값에 따라 바꾸면 되는거 아닌가...? 그럼 새롭게 import할 필요도 없고 기존에 있는 코드를 거의 유지하면서 안 바뀐 부분을 찾는 게 더 빠를 것 같은데 !? 라는 생각이 들어서 바로 실천에 옮겼습니다.
export const white = '#FFF';
export const black = '#000';
export const gray5 = '#BBBBBB';
export const gray10 = '#CCCCCC';
export const gray20 = '#DDDDDD';
현재 앱에선 이렇게 그냥 hex값을 변수에 담아 export를 해주는 방식이었습니다.
만약 이 코드안에 이름은 똑같지만 라이트/다크 모드에 맞게 파싱해서 값을 보내주면..?
이거... 달다..!
먼저 해야할 것을 생각했습니다. 디바이스의 디스플레이가 라이트/다크인지 먼저 가져오자!
export const THEME_KEY = 'THEME';
// AsyncStorage와 같다고 생각하시면 됩니다.
export const themePreference = kvStorage.getString(THEME_KEY) ?? 'system';
export const isLightTheme = themePreference === 'system' ? Appearance.getColorScheme() !== 'dark' : themePreference !== 'dark';
React-native에서 지원해주는 Appearance로 colorScheme를 가져옵니다. 그렇게 라이트/다크 모드를 판단하고 isLightTheme라는 boolean 값을 라이트 모드 일 때만 작성되어야하는 코드들을 판별하기 위해 다른 곳에서 사용이 되어야하니 export를 해줍니다.
그리고 LightColor일떄 white와 DarkColor일때 white를 디자이너분께서 주신 hex code대로 각각 객체의 key value로 만들어서 넣어줍니다.
export const DARK_COLORS = {
// color
white: '#1C1D1F',
...
}
export const LIGHT_COLORS = {
// color
white: '#FFFFFF',
...
}
export const white = isLightTheme ? LIGHT_COLORS.white : DARK_COLORS.white
마지막으로 white는 다른 곳에서 계속 import 되고 있으니 최대한 이 파일에서 모든 처리를 다 해줘서 export 해주면 끝 !
이렇게 되면 기존에 jsx나 ts에서 사용되어지는 파일의 경로, import name을 고칠 필요가 없고 라이트/다크 모드에 맞게 알아서 환경에 맞게 값을 가져오기 때문에 인지적으로 신경쓰며 작업해야할 부분을 많이 없애줬습니다.
이번엔 컬러 시스템의 정의가 갖춰지기 전에 있던 n년전 코드들이 속 썩였습니다.
예를 들어 rgba(0, 0, 0, 1)
이나 backgroundColor: 'white'
등 vscode의 참조로 찾을 수 없는 string 값들이 꽤나 많이 존재했습니다.
이 부분에 대해선 사실 하나하나 찾아가며 바꿔줬습니다. 하지만 조금이라도 효율적으로 해보자해서 검색할 때 'white' 로 검색하거나 rgba( 이렇게 포괄적으로 검색함으로써 많은 걸 찾아낼 수 있었습니다. 물론 정말 힘들었던건 'white'가 backgroundColor만 들어가는게 아니라 스크린명을 보고 찾고 또 찾고 찾고.... 반복이었지만 방법을 바꾸고 신경 써야할게 많이 줄어들어서 충분히 리소스를 투자할 수 있었던 것 같습니다.
물론 지뢰를 찾으면서도 배움이 있었던 게
아무렇지 않게 'white'처럼 냅다 string으로 작성한 컬러값들은 유지보수에 정말 쥐약이구나! 라는 걸 깨닫게 되는 ^^.... 뭐 그런 ....
팀에 개발자가 5명인데 한명씩 다크모드를 작업할 때 플로우를 따르지 않으면 큰 재앙이 되겠죠?
작업하는 것도 중요하지만 공유를 해서 현상을 유지하는 것도 굉장히 중요하다고 생각합니다.
가벼워 보이지만 무거운 2개의 룰을 제가 팀에 공유하려 합니다.
우린 앞으로 일을 더 잘해야만 하기 때문이죠.
이렇게까지 길게 글을 쓰면서도 회고 포인트가 자꾸 생각이 나는군요.
1. 항상 왜?를 고민하자 내가 작업하고 있는 방식이 100% 맞을 순 없습니다. 터널 시야에서 빠져나오려면 메타인지 버튼을 자꾸 눌러줘야한다고 생각합니다. 관련이 없는 제 3자의 시선이 분명히 제게 도움이 될 수도 있으니까요.
2. 섣부르게 추상화/구조화 하지 말자. 섣부른 추상화는 오히려 독이 될 수도 있음을 느꼈습니다. 어? 이렇게 하면 지금 작업은 편하게 할 수 있잖아 ! 라고 했지만 가면 갈수록 추상화를 했는데 반복적으로 쓰는 코드가 많이 지고 나중에 돼서야 아.. 이렇게 하면 내가 내 부하를 높히는구나를 경험했습니다.
가장 크게 느꼈던 두가지인데 텍스트로 보면 그렇게 크게 느껴지지 않을 수도 있지만 정말 현타 많이 왔습니다 ㅎㅎ....
그럼에도 앱 다크모드 스프린트
를 성공적으로 마칠 수 있었던 건 앱을 사용하는 유저들이 더 편하게 앱을 사용하게 되면 어떨까? cs에 제가 개발한 것에 대해 감사하다는 말을 들으면 기분이 정말 좋더라구요. 배포된지 3일 정도 지났으니 다음주 주간회의 cs 모음에 칭찬 하나쯤은 있었으면 좋겠습니다 ㅎㅎ
추가로 이 게시글을 보고 계실지 모르겠지만 다크모드를 만들 때 계속 제 메타인지를 도와주셨던 CTO 민식님 ! 항상 너무 배울게 많고 잘 가르쳐주시는데 제가 많이 못따라가는 것 같습니다. 매번 표현은 적게 하지만 너무 감사해요 🙏🏻 그리고 흔들리는 멘탈 꽉 잡아준 개발팀 모두 감사하고 테스트 트랙에서 계속 디자인 이슈 잡아주신 기획팀 너무 감사했습니다 👍 마지막으로 다크 모드 스크린샷과 함께 타이밍 맞춰서 해외 마케팅 돌려주신 마케팅팀도 너무 고맙습니다 😭
잘보고갑니다 😎