🔑 code is like humor. when you have to explain it it’s bad
💡 코드 품질이란
📃 코드 품질은 소프트웨어가 얼마나 읽기 쉽고, 유지보수하기 쉬우며, 확장 가능하고, 안정적인지를 평가하는 여러 속성의 총합입니다. 예를 들어, 가독성, 일관성, 테스트 용이성, 성능, 그리고 보안 등이 좋은 코드 품질의 중요한 요소입니다
💡 고품질 코드가 중요한 이유
📃 많은 개발자가 고품질 코드가 무엇인지, 어떻게 작성해야 하는지에 대해 고민할 시간이 부족합니다.
이는 명확한 가이드라인의 부재뿐 아니라, 어떤 코드가 고품질인지 인식하지 못하는 문제에서 비롯됩니다.
실제로 매년 많은 개발자들이 더 나은 폴더 구조나 코드 네이밍 컨벤션 등 Best Practice를 찾기 위해 다양한 자료를 검색하고 있습니다
💻깨진 유리창 이론
⬆️ 깨진 유리창 하나가 방치되면 주변 환경의 범죄율이 높아지는 것처럼, 하나의 나쁜 품질의 코드가 다른 부분에 영향을 미쳐 전체 코드 품질을 저하시킬 수 있습니다.
클래스나 모듈의 설계가 명확하지 않거나, 코드가 잘 분리되어 있지 않을 경우 개발자들이 전체적인 문제를 해결하기보다는 당장의 '작은 문제'만 임시로 고치려고 합니다. 결국 이런 임시방편은 근본적인 구조적 문제를 해결하지 못해, 시간이 지나면서 전체 프로덕트의 품질과 유지보수성에 부정적인 영향을 미칠 수 있습니다
💡 복잡해지는 코드와 대규모 시스템
📃 현대 소프트웨어 개발 환경에서는 정교한 알고리즘이 적용되어 한눈에 파악하기 어려운 복잡한 코드와, 단순한 코드들이 모여 복잡한 비즈니스 문제를 해결하기 위해 거대한 시스템을 구축하는 경우가 많습니다.
이때, 단순히 기능 구현에 급급하면 수십만, 수백만 줄의 코드가 얽힌 프로젝트에서 나중에 유지보수나 확장 시 심각한 문제에 직면할 수 있습니다. 이러한 상황에서 코드 설계 지식은 마치 정글 속에서 견고한 건축물을 세우기 위한 튼튼한 기초이자 전략입니다
최근 IT 동향을 살펴보면, 다양한 서비스들을 하나의 모놀리식 프로젝트로 관리하는 데 한계가 있기 때문에, 응집도를 높이고 의존도를 낮추기 위해 마이크로 아키텍처로 전환하는 추세입니다. 마이크로 서비스 아키텍처는 각 서비스가 독립적으로 관리되고 배포될 수 있어, 전체 시스템의 유연성과 확장성을 크게 향상시킵니다
💻Before Refactoring
import './css/DescriptionSection.css';
import HaruhanDescription from '../assets/images/HaruhanPhone.png';
import { useEffect, useRef, useState } from 'react';
const descriptionData = [
{
icon: '🧠',
title: '오늘의 주제',
content: '비트코인, 양자컴퓨터가 해킹하면 3초 만에 증발할까?',
details: [
'기존 컴퓨터로는 사실상 불가능한 비트코인 해킹이 양자컴퓨터 시대가 오면 현실이 될 가능성이 있습니다.',
'현재의 암호화 기술(예: ECDSA)은 양자컴퓨터가 쉽게 풀어버릴 수 있는 구조를 가지고 있어, 보완책이 필요합니다.'
]
},
{
icon: '📌',
title: '알아두면 쓸모 있는 배경 지식',
content: '비트코인의 보안 구조',
details: [
'비트코인은 공개키 암호화(Public Key Cryptography) 기술을 기반으로 합니다. 가장 널리 사용되는 알고리즘이 ECDSA(Elliptic Curve Digital Signature Algorithm, 타원 곡선 서명 알고리즘)인데, 이 기술은 현재의 컴퓨터로는 사실상 해킹이 불가능한 수준입니다.',
'기존의 슈퍼컴퓨터는 비트코인의 개인키를 풀기 위해 수천 년 이상의 시간이 필요하지만, 양자컴퓨터(Quantum Computer)는 완전히 다른 방식으로 연산을 수행하기 때문에, 이 과정을 단 몇 초 만에 해결할 가능성이 있습니다.',
'양자컴퓨터가 문제인 이유: 쇼어의 알고리즘(Shor\'s Algorithm)',
'1994년, 수학자 피터 쇼어(Peter Shor)는 양자컴퓨터가 기존 암호화 방식을 빠르게 해독할 수 있는 알고리즘을 제안했습니다.',
'쇼어 알고리즘을 활용하면 비트코인의 개인키를 단 몇 초~몇 분 만에 추출할 수 있다는 것이 학계의 추정입니다.'
]
}
];
const DescriptionSection = () => {
const [scrollY, setScrollY] = useState(0);
const [isMobile, setIsMobile] = useState(false);
const sectionRef = useRef(null);
useEffect(() => {
// 화면 크기 감지
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024);
};
// 초기 로드 시 한 번 체크
checkMobile();
// 스크롤 이벤트 핸들러
const handleScroll = () => {
setScrollY(window.scrollY);
};
// 리사이즈 이벤트 핸들러
const handleResize = () => {
checkMobile();
};
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
};
}, []);
// calculateBlurEffect 함수 수정
const calculateBlurEffect = () => {
if (!sectionRef.current) return { blur: 0, opacity: 1, bottomFade: 0 };
const rect = sectionRef.current.getBoundingClientRect();
const sectionTop = rect.top;
const sectionHeight = rect.height;
// 모바일과 데스크탑에 따라 다른 임계값 설정
const visibleThreshold = isMobile ? -0.7 : -0.3;
// 섹션이 화면에 들어오기 시작했을 때
if (sectionTop < window.innerHeight && sectionTop + sectionHeight > 0) {
const normalizedPosition = sectionTop / window.innerHeight;
// 섹션이 더 스크롤될수록 하단 페이드 효과 증가
const bottomFade = Math.max(0, Math.min(1, -normalizedPosition * 0.5));
if (normalizedPosition < visibleThreshold) {
const scrollProgress = Math.max(0, Math.min(1,
(visibleThreshold - normalizedPosition) / (Math.abs(visibleThreshold) + 0.5)
));
const maxBlur = 20;
return {
blur: scrollProgress * maxBlur,
opacity: 1 - (scrollProgress * 0.9),
bottomFade: bottomFade
};
}
return { blur: 0, opacity: 1, bottomFade: bottomFade };
}
return { blur: 0, opacity: 1, bottomFade: 0 };
};
const { blur, opacity, bottomFade } = calculateBlurEffect();
return (
<section
ref={sectionRef}
className="DescriptionSectionContainer"
style={{
filter: `blur(${blur}px)`,
opacity: opacity,
transition: 'filter 0.2s ease-out, opacity 0.2s ease-out'
}}
>
<div className="DescriptionSectionTop">
<img className="DescriptionSectionImage" src={HaruhanDescription} alt="하루한 핸드폰" />
<h1 className="DescriptionSectionH1">
원하는 시간에
<span className="DescriptionSectionHighlight"> 메일</span>을 통해 <br />
<span className="DescriptionSectionHighlight">짧지만 알찬 지식</span>을
<br /> 아래와 같이 공유해드려요!
</h1>
</div>
<div className="DescriptionSectionTemplate">
{descriptionData.map((item, index) => (
<div key={index} className="contentBlock">
<h3 className="contentTitle">
{item.icon} {item.title}: {item.content}
</h3>
{item.details && item.details.map((detail, detailIndex) => (
<p key={detailIndex} className="contentDetail">
{detail}
</p>
))}
</div>
))}
</div>
</section>
);
};
export default DescriptionSection;
💻After Refactoring
import React, { useRef } from 'react';
import './css/KnowledgeSection.css';
import useCombinedScrollEffect from '../../hooks/useCombinedScrollEffect';
import KnowledgeOverlay from './KnowledgeOverlay';
import KnowledgeSectionAni from './KnowledgeSectionAni';
import KnowledgeSectionList from './KnowledgeSectionList';
const KnowledgeSection = () => {
const knowledgeRef = useRef(null);
const { blurAmount, descOpacity, closingOpacity, translateY } =
useCombinedScrollEffect(knowledgeRef);
return (
<div className="KnowledgeSection">
<section
ref={knowledgeRef}
className="KnowledgeSectionContainer"
style={{
filter: `blur(${blurAmount}px)`,
opacity: descOpacity,
transition: 'filter 0.3s ease-out, opacity 0.3s ease-out',
}}
>
<KnowledgeSectionAni />
<KnowledgeSectionList />
</section>
{/* KnowledgeOverlay 컴포넌트 */}
<KnowledgeOverlay opacity={closingOpacity} translateY={translateY} />
</div>
);
};
export default KnowledgeSection;
⬆️ 코드 설계에 대한 이해는 단순히 “코드를 잘 짜는” 것을 넘어, 기존 라이브러리나 프레임워크의 내부 동작 원리를 파악하여 문제를 해결할 수 있는 능력으로 이어집니다.
💡 코드 품질 평가의 기준
📃 코드 품질은 단일 기준으로 평가하기 어렵고, 여러 요소를 종합적으로 고려해야 합니다.
다음은 대표적인 평가 기준들입니다
1️⃣ 유지보수성
- 정의: 기존 코드를 버그 없이 수정하고, 기능을 추가할 수 있는 능력
- 중요성: 대부분의 프로젝트는 시간이 지나도 코드가 그대로 남기 때문에,
안정적이고 지속적인 개선을 위해서는 유지보수성이 필수입니다.
2️⃣ 가독성
- 정의: 사람이 이해하기 쉬운 코드
- 중요성: 개발자는 코드를 작성하는 것보다 읽는 시간이 더 많습니다.
가독성이 좋으면 코드 이해와 디버깅, 협업이 쉬워집니다.
3️⃣ 확장성
- 정의: 새로운 기능이나 변경 사항이 발생했을 때, 최소한의 수정으로 대응할 수 있는 능력
- 중요성: 기능 확장이 빈번한 프로젝트에서는 미리 변화에 대비한 설계가 필요합니다.
4️⃣ 간결성
- 정의: 불필요한 복잡함을 배제하고, 단순하면서도 명확하게 코드를 작성하는 것
- 중요성: 복잡성이 낮을수록 이해와 유지보수가 용이해집니다.
5️⃣ 재사용성
- 정의: 기존 코드를 재활용하여 새로운 기능을 구현할 수 있는 능력
- 중요성: 중복 코드를 줄이고, 효율적인 개발을 가능하게 합니다.
6️⃣ 테스트 용이성
- 정의: 작성된 코드에 대해 테스트를 쉽게 작성하고 수행할 수 있는 능력
- 중요성: 낮은 결합도와 모듈화된 구조는 테스트 환경 구축에 큰 도움이 됩니다.
⬆️ 이러한 평가 기준들은 단순히 “좋다/나쁘다”로 이분법적으로 평가하기 어렵고,
연속적인 스펙트럼 상에서 고려되어야 합니다
💡 함께 수정되는 파일을 같은 디렉토리에 두기 (응집도)
📃 코드프로젝트에서 코드를 작성하다 보면 Hook, 컴포넌트, 유틸리티 함수 등을 여러 파일로 나누어서 관리하게 됩니다. 이런 파일들을 쉽게 만들고, 찾고, 삭제할 수 있도록 올바른 디렉토리 구조를 갖추는 것이 중요합니다
함께 수정되는 소스 파일을 하나의 디렉토리에 배치하면 코드의 의존 관계를 명확하게 드러낼 수 있어야 합니다. 그래서 참조하면 안 되는 파일을 함부로 참조하는 것을 막고, 연관된 파일들을 한 번에 삭제할 수 있습니다.
💻 코드 예시
└─ src
├─ components
├─ constants
├─ containers
├─ contexts
├─ remotes
├─ hooks
├─ utils
└─ ...
⬆️파일을 이렇게 종류별로 나누면 어떤 코드가 어떤 코드를 참조하는지 쉽게 확인할 수 없고, 코드 파일 사이의 의존 관계는 개발자가 스스로 코드를 분석하면서 챙겨야 합니다. 또한 더 이상 특정 컴포넌트나 Hook, 유틸리티 함수가 사용되지 않아서 삭제된다고 했을 때, 연관된 코드가 함께 삭제되지 못해서 사용되지 않는 코드가 남아있게 될 수도 있습니다.
프로젝트의 크기는 점점 커지기 마련인데, 프로젝트의 크기가 2배, 10배, 100배 커짐에 따라서 코드 사이의 의존관계도 크게 복잡해질 수 있고, 디렉토리 하나가 100개가 넘는 파일을 담고 있게 될 수도 있습니다
💻 개선한 코드
└─ src
│ // 전체 프로젝트에서 사용되는 코드
├─ components
├─ containers
├─ hooks
├─ utils
├─ ...
│
└─ domains
│ // Domain1에서만 사용되는 코드
├─ Domain1
│ ├─ components
│ ├─ containers
│ ├─ hooks
│ ├─ utils
│ └─ ...
│
│ // Domain2에서만 사용되는 코드
└─ Domain2
├─ components
├─ containers
├─ hooks
├─ utils
└─ ...
⬆️ 함께 수정되는 코드 파일을 하나의 디렉토리 아래에 둔다면, 코드 사이의 의존 관계를 파악하기 쉽습니다
💡매직 넘버 없애기 (가독성)
📃매직 넘버(Magic Number)란 정확한 뜻을 밝히지 않고 소스 코드 안에 직접 숫자 값을 넣는 것을 의미합니다
예를 들어, 찾을 수 없음(Not Found)을 나타내는 HTTP 상태 코드로 404 값을 바로 사용하는 것이나, 하루를 나타내는 86400초를 그대로 사용하는 것이 있습니다
💻 코드 예시
async function onLikeClick() {
await postLike(url);
await delay(300);
await refetchPostLike();
}
⬆️ 300이라고 하는 숫자를 애니메이션 완료를 기다리려고 사용했다면, 재생하는 애니메이션을 바꿨을 때 조용히 서비스가 깨질 수 있는 위험성이 있습니다. 충분한 시간동안 애니메이션을 기다리지 않고 바로 다음 로직이 시작될 수도 있습니다
같이 수정되어야 할 코드 중 한쪽만 수정된다는 점에서, 응집도가 낮은 코드라고도 할 수 있습니다
💻 개선한 코드
const ANIMATION_DELAY_MS = 300;
async function onLikeClick() {
await postLike(url);
await delay(ANIMATION_DELAY_MS);
await refetchPostLike();
}
⬆️ 숫자 300의 맥락을 정확하게 표시하기 위해서 상수 ANIMATION_DELAY_MS로 선언할 수 있습니다
💡같은 종류의 함수는 반환 타입 통일하기 (예측 가능성, 일관성)
📃API 호출과 관련된 Hook들처럼 같은 종류의 함수나 Hook이 서로 다른 반환 타입을 가지면 코드의 일관성이 떨어져서, 같이 일하는 동료들이 코드를 읽는 데에 헷갈릴 수 있습니다
💻 코드 예시 : useUser
다음 useUser 와 useServerTime Hook은 모두 API 호출과 관련된 Hook입니다
그렇지만 useUser는 @tanstack/react-query의 Query 객체를 반환하고, useServerTime은 서버 시간을 가져와서 데이터만 반환하고 있습니다
import { useQuery } from '@tanstack/react-query';
function useUser() {
const query = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
return query;
}
function useServerTime() {
const query = useQuery({
queryKey: ['serverTime'],
queryFn: fetchServerTime
});
return query.data;
}
⬆️ 서버 API를 호출하는 Hook의 반환 타입이 서로 다르다면, 동료들은 이런 Hook을 쓸 때마다 반환 타입이 무엇인지 확인해야 해요. Query 객체를 반환한다면 data를 꺼내야 하고, 데이터만 반환한다면 그대로 값을 사용할 수 있습니다.
같은 종류의 동작을 하는 코드가 일관적인 규칙에 따르고 있지 않으면 코드를 읽고 쓰는 데 헷갈려요
같이 수정되어야 할 코드 중 한쪽만 수정된다는 점에서, 응집도가 낮은 코드라고도 할 수 있습니다
💻 개선한 코드
import { useQuery } from '@tanstack/react-query';
function useUser() {
const query = useQuery({
queryKey: ['user'],
queryFn: fetchUser
});
return query;
}
function useServerTime() {
const query = useQuery({
queryKey: ['serverTime'],
queryFn: fetchServerTime
});
return query;
}