목차
서버 데이터와 컴포넌트의 분리: 유지보수성을 높이는 방법
개발을 진행하다 보면, 서버에서 받아오는 데이터의 형식이 변경되는 경우가 종종 발생합니다. 이러한 변경은 의도치 않은 버그를 유발하거나, 기존 컴포넌트를 수정해야 하는 불필요한 작업으로 이어지기 쉽습니다. 오늘은 서버 데이터와 컴포넌트의 결합을 분리하여 유지보수성을 극대화하는 방법에 대해 자세히 이야기해보고자 합니다.
1. 서론
프론트엔드 개발에서 컴포넌트는 사용자의 인터페이스(UI)를 구성하는 중요한 단위입니다. 보통 컴포넌트는 서버에서 받아온 데이터를 props 형태로 전달받아 화면에 렌더링하게 됩니다. 그런데 문제는, 만약 서버 데이터의 형식이 변경된다면 컴포넌트 내부에서 사용하는 데이터의 구조도 바뀌어야 하는 상황이 발생할 수 있다는 점입니다. 이런 경우 컴포넌트 자체의 책임이 모호해지고, 단일 책임 원칙(Single Responsibility Principle)에도 위배되는 문제가 생깁니다.
오늘은 이를 어떻게 해결할 수 있는지, 그리고 왜 서버 데이터와 컴포넌트의 결합을 분리해야 하는지에 대해 살펴보겠습니다.
2. 서버 데이터와 컴포넌트 결합의 문제점
대부분의 개발자는 초기 개발 단계에서 서버로부터 반환받은 데이터 구조를 그대로 컴포넌트에 전달하는 방식을 사용합니다. 예를 들어, 아래와 같이 Before
라는 타입을 가진 데이터를 서버에서 받아와 컴포넌트에서 그대로 사용하는 경우를 생각해볼 수 있습니다.
interface Before {
name: string;
age: number;
school: string;
friendNumber: number;
howToGetToSchool: string;
}
interface BeforeStudent {
name: string;
age: number;
school: string;
friendNumber: number;
howToGetToSchool: string;
}
const Page = () => {
// 서버에서 데이터를 받아오는 함수
const studentData: BeforeStudent = getStudent();
return <StudentComponent data={studentData} />;
};
interface StudentComponentProps {
data: BeforeStudent;
}
const StudentComponent = ({ data }: StudentComponentProps) => {
return (
<div>
<div>{`이름은 ${data.name} 입니다.`}</div>
<div>{`나이는 ${data.age} 입니다.`}</div>
<div>{`친한 친구의 수는 ${data.friendNumber} 입니다.`}</div>
</div>
);
};
export default StudentComponent;
위와 같이 컴포넌트 내부에서 서버 데이터를 그대로 사용하게 되면, 서버의 반환 타입이 조금만 변경되어도 컴포넌트의 코드를 수정해야 하는 상황이 발생합니다. 예를 들어, 서버에서 반환하는 데이터가 아래와 같이 변경되었다고 가정해봅시다.
interface AfterStudent {
person: {
name: string;
age: number;
school: string;
};
friendNumber: number;
howToGetToSchool: string;
}
이 경우, 기존 StudentComponent
는 BeforeStudent
타입에 의존하고 있으므로, 타입 불일치로 인해 에러가 발생합니다. 개발자는 이를 해결하기 위해 컴포넌트 내부의 데이터 접근 방식을 아래와 같이 수정해야 합니다.
interface StudentComponentProps {
data: AfterStudent;
}
const StudentComponent = ({ data }: StudentComponentProps) => {
return (
<div>
<div>{`이름은 ${data.person.name} 입니다.`}</div>
<div>{`나이는 ${data.person.age} 입니다.`}</div>
<div>{`친한 친구의 수는 ${data.friendNumber} 입니다.`}</div>
</div>
);
};
export default StudentComponent;
비록 위와 같이 수정하는 것이 가능하지만, 문제는 컴포넌트 자체의 역할이 변경된 것이 아니라 단지 서버의 데이터 형식이 변경되었음에도 불구하고 컴포넌트 내부 코드까지 수정해야 한다는 점입니다. 이는 컴포넌트의 유지보수성을 크게 떨어뜨리고, 코드의 재사용성을 저해하는 요인입니다.
3. 문제의 근본 원인: 결합도(Coupling)의 증가
위 사례에서 볼 수 있듯이, 컴포넌트가 서버 데이터의 구체적인 형식에 의존하게 되면, 서버 데이터의 변화에 따라 컴포넌트도 따라 변경되어야 합니다. 즉, 컴포넌트와 서버 데이터 간의 결합도가 높아지는 문제가 발생하는 것입니다. 이러한 결합은 다음과 같은 문제점을 초래합니다.
- 변경 전파의 위험: 서버 데이터의 변경이 컴포넌트의 수정까지 이어지므로, 한 부분의 변경이 시스템 전체에 영향을 미칠 수 있습니다.
- 테스트의 어려움: 컴포넌트가 외부 데이터 구조에 의존하다 보면, 단위 테스트를 작성할 때 모의 데이터(mock data)를 준비하는 것이 복잡해질 수 있습니다.
- 유지보수성 저하: 시간이 지남에 따라 서버 API가 변경되면, 많은 컴포넌트에서 이를 반영하기 위해 수정이 필요해지며, 이로 인해 코드베이스의 유지보수가 어려워집니다.
이러한 문제들을 해결하기 위한 방법 중 하나는 데이터 변환(Data Transformation) 레이어를 도입하는 것입니다.
4. 데이터 변환 레이어 도입: 컴포넌트와 서버 데이터 분리
서버에서 반환받은 데이터와 컴포넌트가 실제로 필요한 데이터의 형태는 반드시 동일할 필요가 없습니다. 오히려 컴포넌트는 자신의 역할(즉, 데이터를 표시하는 것)에만 집중하고, 서버 데이터의 구체적인 구조에 의존하지 않아야 합니다. 이를 위해 데이터 변환 레이어를 도입하여, 서버에서 받아온 데이터를 컴포넌트가 사용하기에 적합한 형태로 가공하는 것이 좋습니다.
아래와 같이 컴포넌트의 props 타입을 컴포넌트 자체에서 필요한 데이터 형태로 정의할 수 있습니다.
interface StudentComponentProps {
data: {
name: string;
age: number;
friendNumber: number;
};
}
const StudentComponent = ({ data }: StudentComponentProps) => {
return (
<div>
<div>{`이름은 ${data.name} 입니다.`}</div>
<div>{`나이는 ${data.age} 입니다.`}</div>
<div>{`친한 친구의 수는 ${data.friendNumber} 입니다.`}</div>
</div>
);
};
export default StudentComponent;
그리고 서버 데이터가 변경되었을 때, 컴포넌트를 사용하는 상위 컴포넌트(Page)에서 아래와 같이 데이터 변환을 수행합니다.
interface AfterStudent {
person: {
name: string;
age: number;
school: string;
};
friendNumber: number;
howToGetToSchool: string;
}
const Page = () => {
// 서버에서 데이터를 받아오는 함수
const studentData: AfterStudent = getStudent();
// 데이터 변환: 서버 데이터 -> 컴포넌트에서 필요한 데이터 형태로 가공
const data = {
name: studentData.person.name,
age: studentData.person.age,
friendNumber: studentData.friendNumber,
};
return <StudentComponent data={data} />;
};
이렇게 하면, 서버 데이터의 변경이 발생하더라도 StudentComponent는 그대로 유지될 수 있습니다. 즉, 컴포넌트는 오직 자신의 역할(데이터 렌더링)에 집중하며, 데이터 변환 로직은 별도의 책임 영역으로 분리됩니다.
5. 컴포넌트와 데이터의 단일 책임 원칙
위 사례를 통해 우리는 단일 책임 원칙(Single Responsibility Principle)의 중요성을 다시 한번 확인할 수 있습니다. 단일 책임 원칙에 따르면, 한 모듈은 오직 하나의 책임만 가져야 합니다. 컴포넌트는 UI를 렌더링하는 역할에 집중하고, 데이터의 변환이나 가공은 다른 레이어나 헬퍼 함수에서 처리하는 것이 바람직합니다.
- 컴포넌트의 책임: 전달받은 데이터를 화면에 올바르게 표시하는 것.
- 데이터 변환 레이어의 책임: 서버에서 받은 데이터를 컴포넌트가 사용할 수 있는 형태로 가공하는 것.
이러한 역할 분리는 각 요소가 독립적으로 관리될 수 있게 하며, 어느 한쪽에 변경이 발생해도 다른 쪽에 미치는 영향을 최소화합니다. 결과적으로 전체 시스템의 유지보수성과 확장성이 크게 향상됩니다.
또한, 이러한 구조는 팀 내에서의 협업에도 긍정적인 영향을 줍니다. 백엔드 개발자가 서버 API를 변경하더라도, 프론트엔드 팀은 데이터 변환 로직만 수정하면 되기 때문에, 컴포넌트 자체의 수정 부담이 줄어듭니다. 반대로, UI 변경이 필요한 경우에도 데이터 변환 로직은 그대로 유지할 수 있습니다.
6. 실전 적용 시 고려할 사항
데이터 변환 레이어를 도입할 때 몇 가지 고려해야 할 점들이 있습니다.
- 중앙 집중식 데이터 매핑 관리: 프로젝트 규모가 커지면, 여러 컴포넌트가 다양한 서버 데이터를 사용하게 됩니다. 이때 데이터 매핑 로직을 중앙에서 관리하거나, 헬퍼 함수 혹은 유틸리티 모듈로 분리하는 것이 좋습니다. 이렇게 하면 데이터 형식 변경에 따른 수정 범위를 최소화할 수 있습니다.
- 타입 안정성 확보: TypeScript와 같은 정적 타입 시스템을 사용하고 있다면, 서버 응답 타입과 컴포넌트에서 사용하는 타입을 명확히 분리하고, 변환 함수에 대한 타입 정의를 철저히 하는 것이 좋습니다. 이는 런타임 에러를 예방하고, 코드의 가독성을 높이는 데 큰 도움이 됩니다.
- 테스트 코드 작성: 데이터 변환 로직이 별도로 존재하면, 해당 로직에 대한 단위 테스트를 작성하여 안정성을 확보할 수 있습니다. 테스트를 통해 데이터 매핑이 올바르게 이루어지는지 검증하면, 서버 API 변경 시에도 빠르게 문제를 파악하고 대응할 수 있습니다.
- 문서화: 데이터 변환 로직과 컴포넌트가 사용하는 데이터 형식을 명확하게 문서화해 두면, 새로운 팀원이 프로젝트에 참여할 때도 전체 구조를 이해하는 데 큰 도움이 됩니다.
7. 결론
서버 데이터와 컴포넌트를 분리하는 것은 단순히 코드를 깔끔하게 만드는 차원을 넘어, 유지보수성과 확장성을 크게 향상시키는 핵심 전략입니다. 컴포넌트는 오직 UI 렌더링이라는 단일 책임에 집중하고, 서버에서 받은 데이터는 별도의 변환 레이어를 통해 컴포넌트가 필요로 하는 형태로 가공하는 것이 바람직합니다.
이와 같은 구조적 분리를 통해, 서버 API가 변경되더라도 컴포넌트의 내부 코드는 건드리지 않아도 되고, 오직 데이터 변환 로직만 수정하면 되는 장점이 있습니다. 또한, 이는 팀 내 협업과 코드의 테스트 및 유지보수 측면에서도 많은 이점을 제공합니다.
프론트엔드 개발자라면, 프로젝트 초기 단계에서부터 이러한 설계 원칙을 적용해 보는 것을 강력히 추천합니다. 단일 책임 원칙을 준수함으로써 코드의 재사용성을 높이고, 변경에 유연하게 대응할 수 있는 구조를 만드는 것은 장기적으로 개발 생산성을 크게 향상시킬 것입니다.
'Dev' 카테고리의 다른 글
리눅스 Mattermost HTTPS(SSL)로 운영하는 방법 (0) | 2024.08.15 |
---|---|
2024년 모바일 앱 개발의 최신 트렌드 (0) | 2023.12.25 |
session method 세션 메소드 .setAttribute() .getAttribute() .invalidate() .setMaxInactiveInterval() .isNew() (0) | 2023.07.31 |
티스토리 jsFiddle 연동 JS피들 사용법 블로그에 코드 공유하는 방법 codePen CodeSandbox. 코드블럭 (0) | 2023.07.07 |
댓글