[Frontend Fundamentals] 코드의 가독성을 위한 맥락 줄이기
* Toss의 Frontend Fundamentals를 읽으며 메모 및 공부한 내용입니다.
함께 실행되지 않는 코드는 분리하기
분기가 나뉘는 등 동시에 실행되지 않는 코드가 하나의 함수 혹은 하나의 컴포넌트에 있다면, 그 요소의 동작/역할을 파악하기 어렵다.
만약 사용자의 권한에따라 렌더링되는 버튼이 다르다면 아래처럼 구현할 수 있다.
function SubmitButton() {
const isViewer = useRole() === "viewer";
useEffect(() => {
if (isViewer) {
return; // A케이스
}
showButtonAnimation(); // B케이스
}, [isViewer]);
return isViewer ? (
<TextButton disabled>Submit</TextButton> // A케이스
) : (
<Button type="submit">Submit</Button> // B케이스
);
}
하지만 위 예시는 사용자의 권한에따라 분기가 나눠지는 동작이 교차되어 나타난다. (A/B/A/B)
이 경우 코드를 읽는 입장에서 고려해야할 맥락이 많아지므로 두 버튼을 완전히 분리하여 분기를 줄일 수 있다.
function SubmitButton() {
const isViewer = useRole() === "viewer";
return isViewer ? <ViewerSubmitButton /> : <AdminSubmitButton />;
}
function ViewerSubmitButton() {
return <TextButton disabled>Submit</TextButton>;
}
function AdminSubmitButton() {
useEffect(() => {
showAnimation();
}, []);
return <Button type="submit">Submit</Button>;
}
ViewSubmitButton과 AdminSubmitButton은 함께 동작하지 않으므로, 아예 별도의 컴포넌트로 분리한다.
이렇게 개선하는 경우 하나의 분기만 고려하며 읽으면 된다!
구현 상세 추상화하기
하나의 컴포넌트/함수 등에 6~7개 이상의 맥락이 포함되는 것을 지양하자.
내 코드를 읽는 사람이 코드를 쉽게 읽을 수 있도록 맥락을 추상화해보자!
function LoginStartPage() {
useCheckLogin({
onChecked: (status) => {
if (status === "LOGGED_IN") {
location.href = "/home";
}
}
});
/* ... 로그인 관련 로직 ... */
return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
}
사용자가 로그인 된 상태인지 확인하고, 로그인이 된 경우 /home으로 이동시키는 페이지다.
이 페이지의 동작을 파악하기 위해서는 status, onChecked, useCheckLogin 등을 모두 파악해야한다.
이렇게 하나의 컴포넌트에 모든 동작이 노출되어있다면 HOC, Wrapper 컴포넌트 등을 활용해 추상화해 개선할 수 있다.
Wrapper 컴포넌트 사용하기
AuthGuard 컴포넌트가 로그인 여부를 확인하고, 로그인되지 않은 사용자만 자식 컴포넌트(로그인 페이지)를 렌더링하도록 분리한 방식이다.
function App() {
return (
<AuthGuard>
<LoginStartPage />
</AuthGuard>
);
}
function AuthGuard({ children }) {
const status = useCheckLoginStatus();
useEffect(() => {
if (status === "LOGGED_IN") {
location.href = "/home"; // 로그인된 경우 홈으로 이동
}
}, [status]);
return status !== "LOGGED_IN" ? children : null;
}
function LoginStartPage() {
/* ... 로그인 관련 로직 ... */
return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
}
로그인 여부를 확인하는 컴포넌트와, 사용자가 로그인하기 위한 UI, 로직 등이 담긴 컴포넌트로 분리됐다.
각 컴포넌트의 역할을 파악하는 데 필요한 맥락이 줄어든 것을 포함해 또다른 장점이 있다.
- LoginStartPage를 다른 곳에서도 재사용할 수 있음
- LoginStartPage는 이제 유저가 '로그인 된 상태인지' 신경쓰지 않아도 됨
* AuthGard는 { children }을 받고 있는데, 이는 Wrapper 컴포넌트로서 자식 컴포넌트를 받아 렌더링하기 위해 사용한다.
즉, 이 예시에서는 LoginStartPage를 렌더링한다.
HOC (Higher-Order Component) 사용하기
HOC은 컴포넌트를 감싸서 특정 기능을 추가하는 패턴이다.
이 예시를 개선하기 위해 컴포넌트를 감싸는 함수를 만들어 로그인 여부를 확인하는 기능이 추가됐다.
function LoginStartPage() {
/* ... 로그인 관련 로직 ... */
return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
}
export default withAuthGuard(LoginStartPage);
// HOC 정의
function withAuthGuard(WrappedComponent) {
return function AuthGuard(props) {
const status = useCheckLoginStatus();
useEffect(() => {
if (status === "LOGGED_IN") {
location.href = "/home"; // 로그인된 경우 홈으로 이동
}
}, [status]);
return status !== "LOGGED_IN" ? <WrappedComponent {...props} /> : null;
};
}
LoginStartPage를 export할 때 withAuthGuard라는 함수에 감싸져 내보내도록 되어있고,
withAuthGuard는 사용자의 로그인 상태를 확인해 최종적으로 렌더링될 컴포넌트를 반환한다.
이제 LoginStartPage 컴포넌트를 사용할 곳에서 <LoginStartPage/> 만 명시해줘도 AuthGuard까지 내부적으로 호출되는 것이다.
HOC 패턴을 사용하는 경우, 아래와 같은 장점이 추가된다.
- LoginStartPage뿐만 아니라 다른 컴포넌트에서도 AuthGuard 기능이 필요할 때 재사용 가능 (ex. export default withAuthGuard(OtherPage);)
- 로그인 검증 로직을 여러 곳에서 중복하지 않고 한 군데에서 관리할 수 있음
장단점
Wrapper 컴포넌트와 HOC 모두 가독성을 높일 뿐만 아니라 다른 컴포넌트에도 쉽게 적용 가능하며 로그인 검증 로직을 한 곳에서 관리할 수 있다는 장점이 있다.
현재 예시로 가장 와닿는 두 방식의 차이점은 사용방식인데, 이 관점에서 필요에따라 사용해볼 수 있을듯 하다
✅ Wrapper 컴포넌트
- 여러 컴포넌트를 한 번에 감쌀 수 있어서 편리함
- 명시적으로 컴포넌트를 감싸야하기 때문에 코드 흐름이 명확함
- 컴포넌트를 감싸는 부분이 많아지면 JSX가 깊어질 수 있음!!
✅ HOC
- 한 번 감싸면 꼭 필요한 동작을 신경 쓸 필요 없이 자동 적용됨
- 여러 컴포넌트에 그 동작을 재사용 가능
- 임포트해 컴포넌트를 사용하는 시점에서는 감싸진 동작이 보이지 않아 흐름을 이해하기 어려울 수 있음!!