
1. 배경
내가 맡은 페이지의 대다수는 DB 에 데이터를 등록하고, 수정하고, 삭제하는 기능을 지닌다. 때문에 DB 가 바뀌거나, 정책이 바뀌거나 하면 내 페이지는 거의 필수적으로 수정을 해야 한다. 심지어 이 프로젝트는 어떻게 보면 처음 만드는 거고, 고객들의 요구사항을 많이 받지 못해 우리끼리 스스로 정책을 읽고 스스로 요구 사항을 만들어 내며 기획을 했기에 덜 전문적이었다. 그렇게 개발된 페이지들이다 보니 실제 서비스를 운영하면서 기획을 수정해야 하거나 고쳐야할 것들이 많았다. 그중에 몇 가지 정말 강렬한 인상을 남긴 페이지들이 있다.
2. DB 구조가 자주 바뀌는 경험
2-1) 상황
내가 기획하고, 디자인하고, 개발하는데 가장 힘들었던 페이지 중 하나가 바로 발전소를 등록, 수정, 삭제하는 페이지다. 우리 도메인에서 발전소는 굉장히 중요한 데이터다. 거의 모든 컨텐츠가 발전소 중심이다. 처음에 기획할 때만 하더라도 발전소에 그렇게 많은 것들이 덕지덕지 붙어있진 않았다. 순수하게 발전소가 있고, 발전소 하위에 소규모 전력자원들이 존재하고, 그 소규모전력자원들의 타입이 조금씩 다르고.. 뭐 이정도? 그러니까 단순히 발전소 하나에 매핑 구조가 1 : N 인 테이블이 하나 있는 정도였다.
하지만, 시간이 지날수록, 우리가 정책을 배우면 배울수록 발전소에 뭐가 덕지덕지 많이 붙어야만 했다. 지금 누구를 탓하려는 건 아니지만, 여러 정책이 생기고 우리가 제공하는 소프트웨어 및 서비스가 늘어나면서 구조가 복잡해지기 시작한다. 어쩔 수가 없다. 입찰제도에 참여 하는지 안 하는지, KPX 에 등록했는지 안 했는지, 발전소 실제 용량과 계약 용량은 다르게 할 수도 있고... 초기에는 1 : N 으로 매핑했는데 나중에는 1 : 1 로 매핑했다가 그 다음에 다시 어쩔 수 없이 1 : N 으로 매핑하는데 여기에서 N 은 최대가 2이고, 서로는 각자 다른 타입이고.. 자세하게 설명할 수는 없지만, DB 구조가 이랬다, 저랬다 바뀌었다.
이 DB 구조에 맞춰서 화면을 재 설계 하는 것은 오로지 내 몫이다. 이게 컨텐츠가 하나 추가되면 그냥 단순하게 컴포넌트 추가 하면 되지 라는 생각을 하고 있었는데, 그게 아니다. 진짜 고려할 거 많다.. 이게 Optional 한 선택지라면, 그에 따른 분기 처리를 어떤 식으로 사용자에게 보여줘야 할지도 고려해야 하고, 그런데 어떤 경우에는 Optional 한 선택지가 필수적인 선택지가 될 때도 있고... , 매핑 구조가 1:N 이냐, 1:1 이냐, 1:N(2) 이냐에 따라 UX 적인 측면에 보이는 것이 달라야 했고 그에 맞춰서 다시 디자인하고, 그러면 그에 맞춰서 다시 코드를 추가/수정하고 타입으로 지정한 인터페이스를 고치고, 그러면 또 인터페이스 고친 것에 따라서 코드르 수정 쫙 하고. 뭔가는 더해지고, 뭔가는 빼지고. 내가 레이아웃을 다시 처음부터 설계한 횟수를 세다가 멈췄는데 마지막으로 센 횟수가 4번이었다. 그 이후에는 한 세 번 정도 수정한 것 같다.
2-2) 해결하기 위한 노력
(1) 일단 주석 덕지덕지 붙이고, 틀을 만들어라.
하도 많이 바뀌기 때문에 무엇부터 건드려야 하나 고민이 될 때가 있다. 특히 상태를 관리하는 Pinia 내부에서는 굉장히 복잡하게 코드들이 섞여 있다. 해서, 나는 먼저 템플릿을 작성했다.
{
"store template": {
"prefix": "stt",
"body": [
"/* LIB */",
"import { defineStore } from 'pinia'",
"",
"/* COMPONENT */",
"",
"/* FETCH */",
"",
"/* MODULE */",
"",
"/* LOCAL */",
"",
"/* TYPE & IMPL */",
"",
"/* PREPARE TOOL */",
"",
"export default defineStore('[${1:}] ${TM_FILENAME_BASE}', () => {",
" /*==================================",
" (0) ALREADY FETCHED ",
" ===================================*/",
"",
" /*==================================",
" (1) FETCHED DATA ",
" ===================================*/",
"",
" /*==================================",
" (2) INTERACTION DATA ",
" ===================================*/",
"",
" /*==================================",
" (3) VALIDATION",
" ===================================*/",
"",
" /*==================================",
" (4) RESET TOOL",
" ===================================*/",
"",
" return {",
" // (0)",
" ",
" // (1)",
"",
" // (2)",
"",
" // (3)",
"",
" // (4)",
" }",
"})"
],
"description": "pinia store template"
}
}
이런 식으로 각각의 카테고리를 만들고, 개발할 때 수정할 곳이 어디인지를 빠르게 파악할 수 있도록 해놨다. 그리고 함수에 주석을 붙이면서 어떤 기능을 하는지 써주었다. 만약 분기문이 있다면 이 분기문이 왜 필요한지, 그리고 각 if() 안에 작성된 것이 무엇을 의미하는 것인지를 적어주었다.
/**
* 첫 번째 인자가 두 번째 인자보다 작을 때 둘을 더하는 함수
* @param : 첫 번째 인자가 두 번째 인자보다 작아야 한다.
*/
const exampleAdd = (smallerNum: number, biggerNum: number) => {
// (1) 만약 smallerNum 이 biggerNum 보다 크면?
// >> 인자가 이렇게 들어왔다면 이 함수를 사용하는 곳에서 문제가 발생했다는 것
// >> 에러를 발생시켜야 한다.
if(smallerNum >= biggerNum) {
throw new Error('[exampleAdd] 첫 번째 인자가 두 번째 인자보다 클 수 없습니다.')
}
// (2) smallerNum 이 biggerNum 보다 작다면?
// >> 정상적인 상황이므로 더한다.
return smallerNum + biggerNum
}
간략한 예시를 들면 위의 코드처럼 작성했다. 이런식으로 주석과 틀을 만들어서 최대한 어떤 것을 건드려야 할지 빠르게 한글로 파악할 수 있도록 해놨다.
(2) API 응답 값은 그대로 보존할 것. FE 에서 사용할 값은 따로 계산할 것.
API 의 응답값으로 받은 것은 Store 에서 그대로 저장한다. 그리고 그 API 응답값을 계산한 값을 computed 로 따로 계산해서 사용했다. 즉, API 응답값을 직접 수정해서 사용한 적은 없다. API Response 로 받은 Interface 가 있다면, 그 Interface 에서 파생해서 프론트엔드에서 사용하고자 하는 형태로 Interface 를 새로 만들든, 가공을 한 새로운 Interface 를 만들든 해서 API 응답값은 그대로 보존하도록 해놨다.
이게 좀 중요했던 이유 중의 하나는, 단순히 Read 하는 것이 아니라 수정도 하고, 새로 생성도 해야 하는 페이지다 보니 원본 데이터가 훼손되면 안 된다. 수정하다 취소하면 원래대로 되돌리는 작업도 해야 하고, 사용자가 API 로부터 넘겨받은 데이터를 직접 수정한다는 것도 데이터를 훼손하는 것처럼 느껴졌다.
또, 이게 중요했던 이유 중 하나는 API Response 의 구조는 자주 바뀌는데, 화면 상에서는 아무것도 바뀌지 않을 때가 있다. 그러니까 뭐가 추가가 돼서 JSON 의 구조가 변경됐는데 추가됐다고 해서 지금 현재 보이는 컨텐츠가 변경되지 않는 경우. 이런 경우, API 에서 계산하는 로직만 변경하면 간편하게 수정할 수 있었다. 즉, 화면에 보이는 데이터 구조와 API 로 넘겨 받은 데이터 구조가 항상 일치하지 않더라도 그 중간 인터페이스로 계산식만 바꾸면 되니 쉽게 변경이 가능했다는 것이다.
3. 기능 추가요~ 또 추가요~ 하나 더요~ 아 진짜 마지막이요~ 미안. 싹 바꿔야 겠다.
3-1) 상황
위의 제목. 거짓말 같아 보이는가. 실화다.
어쩔 수 없다. 이건 진짜 어쩔 수 없었다. 나도 납득하면서 수정했기 때문에. 위의 2번째 상황과 비슷한 상황이긴 한데, 기능이 정말 폭발적으로 늘어나는 페이지가 하나 있었다.
이건 RTU 와 관련된 페이지였다. 우리 회사 도메인 중에는 RTU 로 데이터를 수집하고 컨트롤 할 수 있는 소프트웨어와 자체 솔루션을 갖고 있다. 그렇기에 맨 처음에 기획했을 때는, 해당 도메인 서버로부터 API 로 데이터를 받아서 그냥 보여주기만 하고, 그 외 간단한 기능만 추가했다.
그런데... 이게 일이 점점 커졌다. 처음 시작은 그냥 단순히 READ 하는 것만 할 것이 아니라 실제로 등록하는 기능까지 추가하자로 갔다가, 갑자기 관련된 장치가 더 추가 됐다가, UI/UX 디자인도 싹 바꿨다가 소프트웨어 버전 및 다운로드 기능까지 넣었다가 인증 까지 넣었다가, 더 추가하고 넣고 바꾸고 했는데. 저번 주 주간회의에서 들은 말은 "이거 OOO 가 추가돼서 완전 바꿔야 돼요"
와....
근데.. 어쩔 수가 없다. 그치 어쩔 수가 없지. 사용자가 원하는 건데. 그리고 정책이 추가가 된 건데. 당연히 반영해야지. 그거에 대해서는 불만이 없지만, 개발하는 사람 입장에서는 큰 고민이 생겨나게 된다. 바로 코드가 점점 불어나서 이게 무슨 코드인지 이해가 안 된다는 것이다. 하나의 함수에 하나의 if 문 혹은 기능이 더 추가되고, 또 추가되고, 또 추가되고... 이렇게 되다 보니 함수 하나가 너무 커지는 경우도 있었고, 이제 이 함수가 무슨 기능을 하는 건지 헷갈리기도 하고, 나중에 수정했을 때 어디 부분을 수정해야하는지 가늠이 안 될 때가 있었다.
3-2) 해결하기 위한 노력
(1) 함수 쪼개. 더 쪼개. 더더더 쪼개!!!!
함수형 프로그래밍과 선언적 프로그래밍 방식을 공부한 경험이 빛을 발휘한 순간이었다. 나는 최대한 함수 하나에 하나의 기능만 하자는 마인드로 코드를 짰다. 그러니까 차라리 함수를 합성하거나, 조합하거나, 함수를 나열하는 한이 있더라도, 함수 몸체에서 작성되는 코드의 양을 확 줄여 나갔다. 그리고 최대한 함수의 이름을 직관적으로 알 수 있도록 사용했다. 그냥 이게 하나의 기능을 한다면, 하나의 함수로 만들었다.
여기서 중요한 점은, 1:1 대응의 순수 함수로 만들기 위해 노력했다. 이게 사실 좀 귀찮은 작업이긴 했다. 아니 어차피 함수 몸체 바깥에 바로 조회가 가능한 데이터가 있는데 굳이, 이걸 순수함수로 만든다? 근데, 이렇게 해야 분리가 된다. 만약에 함수 외부에 있는 상태 값을 참조하게 되면, 그 함수는 해당 상태 값에 의존성이 생겨서 다른 폴더로 분리하거나 떨어트려서 관리할 수 가 없다. 내가 원했던 것은 직관적으로 이해되는 코드다. 그리고 최대한 사이드 이펙트를 줄이는 코드다. 그렇기 때문에 조금 많이 귀찮은 작업이라 하더라도 최대한 넘겨받는 인자 값과 내뱉는 값이 항상 일치하게, 그리고 외부에는 아무런 변화를 주지 않도록 순수함수를 많이 사용해서 개발했다.
그렇다고 모든 함수를 순수함수로 만들지는 못한다. 안 그래도 가뜩이나 개발 시간이 촉박한데 개발 생산성을 위해서도, 그리고 애초에 하나의 상태에만 의존적이고 재활용될 일이 없는 함수인 경우 그냥 의존하도록 만드는 것도 나쁘지 않다고 생각한다. 하지만, 함수로 쪼개서 어떤 역할을 하는지 명시하고 하나의 기능만을 수행하고, 순수하게 만들고 폴더나 파일로 코드를 나눔으로써 계속해서 기능이 추가되더라도 좀더 직관적으로 코드를 읽어낼 수 있도록 노력했다.
3-3) 했어야 했던 것. 혹은 앞으로 할 것
(1) 테스트 코드 작성
테스트 코드를 작성하면 더 마음이 편했을 것 같다. 아무리 순수하게 함수를 만든다고 해도, 그 함수를 수정해야 하는 경우도 많이 있었다. 그럴 때마다 사이드 이펙트가 발생하지는 않을까 걱정을 했다. 그리고, 뭔가 기능이 계속 추가 될 때마다 기존의 기능이 아무런 문제 없이 잘 동작하는지에 대한 우려도 많이 있었다. 실제로 하나 기능 추가 될 때마다 원래 기능들이 잘 동작하는지 테스트를 거쳐야 했다. 그런 것들을 좀 테스트 코드로 자동화하면 좋았을텐데...
단위 테스트, 통합 테스트, E2E 테스트 들에 대해서 좀더 공부하고 우리 도메인에 도입하면 좋을 거 같다는 생각이 들었다.
'je개발 회고' 카테고리의 다른 글
[ je 개발 회고 ] L 프로젝트 (5) - Vue 로 방대한 양의 데이터 다룰 때 주의할 점 (1) | 2024.03.06 |
---|---|
[ je 개발 회고 ] L 프로젝트 (4) - 페이지 루프 기능 도입 (0) | 2024.03.05 |
[ je 개발 회고 ] L 프로젝트 (2) - 기획과 디자인까지 제가 다 하는 거군요. 예. (2) | 2024.02.21 |
[ je개발 회고 ] L 프로젝트 (1) - Overview (0) | 2024.02.20 |
[ je개발 회고 ] Cherryblossom (3) - 아쉬운 점 (0) | 2023.10.09 |