리액트 개발 착수
플레시 카드 프로젝트 프론트엔드 착공시작하고 겪은 일과 생각들입니다.
MSW Warning: captured a request without a matching request handler:
MSW Warning: captured a request without a matching request handler:
MSW를 사용하는데 이런 경고 메시지를 받았습니다.
일단 해결은 상당히 단순했습니다.
if (process.env.NODE_ENV === 'development') {
worker.start({ onUnhandledRequest: 'bypass' });
}
MSW logging warnings for unhandled Supertest requests
그래서 일단 Stack overflow에서 검색하고 해결책을 바로 적용했습니다.
하지만 의문이 있습니다. 그래서 일단 chatGPT에 질문했습니다.
MSW Warning: captured a request without a matching request handler:
이런 경고 메시지를 받았습니다.
일치하는 해더 없이 요청을 포착했다고 의미하는가?
만약 일치하는 해더 없이 요청을 포착했다면 MSW로 mocking하는데 무슨 문제가 있는 것인가? 아니면 무슨 문제가 생길 수 있는가?
"MSW"는 Mock Service Worker의 약어로, 개발 및 테스트 목적으로 실제 서버 요청을 가로채고 가짜 응답을 제공하는 도구입니다. 경고 메시지 "Warning: captured a request without a matching request handler"는 MSW에서 발생하는 것으로, 요청을 가로챘지만 해당 요청에 대한 처리기(handler)가 없다는 것을 의미합니다.
이러한 경고 메시지가 나타나는 이유는 다음과 같습니다:
- 요청을 가로채기 위해 등록한 모의(request)와 실제로 수신된 요청이 일치하지 않을 때 발생할 수 있습니다. 예를 들어, 등록한 모의 요청과 메서드, 경로 또는 헤더 등이 일치하지 않는 요청이 발생하면 이 경고가 표시됩니다.
- MSW에서 등록한 요청 처리기(handler)가 설정되지 않은 경우에도 이 경고가 나타날 수 있습니다. 요청을 가로챌 때 해당 요청에 대한 처리 방법을 정의하는 요청 처리기가 등록되어 있어야 합니다.
이러한 문제가 발생할 경우 MSW mocking에 문제가 있는 것입니다. 이는 실제 서버 응답을 제공하는 대신 가짜 응답을 반환하도록 구성되어 있으므로, 등록된 요청 처리기가 없으면 원하는 응답을 제공할 수 없게 됩니다. 따라서 응답을 처리할 수 있는 적절한 요청 처리기를 등록하는 것이 중요합니다.
문제가 생길 수 있는 가능성은 다음과 같습니다:
- 실제 서버와의 통신이 필요한 기능을 모킹하고 있을 때, 해당 요청을 처리하기 위해 등록된 처리기가 없으면 올바른 응답을 받을 수 없게 됩니다.
- 요청 처리기가 부적절하게 구성되어 있다면, 가짜 응답이 실제 서버의 동작과 일치하지 않을 수 있습니다. 이는 개발자가 응답을 테스트하거나 디버깅할 때 문제를 일으킬 수 있습니다.
따라서 MSW를 사용할 때는 등록된 요청 처리기가 요청과 일치하도록 확인하고, 필요한 요청에 대해 적절한 가짜 응답을 제공할 수 있도록 설정해야 합니다.
- chatGPT
MSW로 서버를 흉내내는데 잘못 흉내내는 문제라고 생각할 수 있습니다. 다양한 대응 방침이 있습니다. 일단 단순하게 기능을 구현하고 통신 동작에 익숙해지면 추가하는 전략이 있습니다.
사족으로 MSW로 조기에 설정할 필요는 없습니다. 프로젝트가 성숙해졌을 때 도입해도 늦지 않을 것 같습니다.
handler 관심사 분리
handler 관심사 분리는 중요합니다. 저는 개인프로젝트이지만 현업에서 mocking할 백엔드 코드의 숫자는 엄청나게 많습니다. 그래서 관심사를 분리하는 것이 중요합니다.
// handlers/index.ts
import type { DefaultBodyType, MockedRequest, RestHandler } from 'msw';
import * as cardHandlers from './cardHandlers';
import * as authHandlers from './authHandlers';
export const handlers: RestHandler<MockedRequest<DefaultBodyType>>[] = [
...Object.values(cardHandlers),
...Object.values(authHandlers),
];
여기서도 인덱스 엔트리를 잘 활용하는 것이 중요합니다.
위 아티클을 보고 따라했습니다.
인덱스 엔트리
// component/Navbar/index.tsx
function Navbar() {
return <nav>home</nav>;
}
export { Navbar };
// component/index.ts
export * from './Navbar';
// Layout.tsx
import { Global } from '@emotion/react';
import GlobalStyle from '../styles/Reset';
import { Navbar } from '../Components';
function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Global styles={GlobalStyle} />
<Navbar />
{children}
</>
);
}
export default Layout;
기존보다 index 엔트리를 더 간결하게 작성하는 방법을 적용했습니다.
주의할 점이 있습니다. 만약에 index
를 tsx
확장자로 선언하면 다음 에러가 발생할 것입니다.
This rule can't verify that
export *
only export components
인덱스 엔트리 설정할 때 항상 ts
확장자입니다. 이런점을 조심하도록 합시다.
스타일링 관심사 분리
Styled
에서 접근하지 말고 바로 컴포넌트명 그래도 활용하는 것이 좋겠습니다. 가독성을 너무 저해합니다. 또 인덱스 엔트리 배웠다고 바로 활용하는데 꼭 좋은 것 같지는 않습니다.
스타일링과 관련된 코드를 다른 모듈로 분리해서 마크업 정보를 담는 관심사와 스타일링을 담는 관심사를 분리하는 것은 좋습니다.
의도는 좋은데 막상 해보니까 가독성이 너무 없습니다. 모든 것을 인덱스 엔트리로 처리할 필요는 없습니다.
// /Components/Navbar/index.ts
import * as Styled from './Navbar.style';
function Navbar() {
return (
<Styled.Navbar>
<Styled.Container>
<Styled.LeftList>
<Styled.ListItem>home</Styled.ListItem>
<Styled.ListItem>Deck</Styled.ListItem>
</Styled.LeftList>
<Styled.RightList>
<Styled.ListItem>Setting</Styled.ListItem>
</Styled.RightList>
</Styled.Container>
</Styled.Navbar>
);
}
export { Navbar };
// /Components/Navbar/Navbar.style.tsx
import styled from '@emotion/styled';
export const Navbar = styled.nav`
height: 4rem;
width: 100%;
background-color: #f8f8f8;
display: flex;
align-items: center;
padding: 0 2rem;
`;
export const Container = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
`;
export const LeftList = styled.ul`
display: flex;
`;
export const RightList = styled.ul``;
export const ListItem = styled.li``;
Styled를 접두어를 붙이는 코드를 봤습니다. 가독성을 해칩니다.
일단 익숙한 JSX 문법으로 스타일을 작성하고 활용한다는 점은 좋습니다.
RRD 라우팅
너무 오래간만에 해서 조금 당황스럽습니다.
Link 태그 사용하면 해결되는 문제였습니다.
import { Link } from 'react-router-dom';
function Navbar() {
return <Link to={'/cards'}>Home</Link>;
}
export { Navbar };
생각보다 단순하게 해결할 수 있었습니다.
조건부 랜더링 관심사 분리
import { Link } from 'react-router-dom';
import { Nav, Container, List, ListItem } from './Navbar.style';
import { useLogin } from '../../hooks';
function Navbar() {
const { isLoggedIn } = useLogin();
return (
<Nav>
<Container>
{isLoggedIn ? (
<>
<List>
<ListItem>
<Link to={'/cards'}>Home</Link>
</ListItem>
<ListItem>
<Link to={'/deck'}>Deck</Link>
</ListItem>
</List>
<List>
<ListItem>
<Link to={'/setting'}>Setting</Link>
</ListItem>
</List>
</>
) : (
<>
<List>
<ListItem>
<Link to={'/'}>Home</Link>
</ListItem>
</List>
<List>
<ListItem>
<Link to={'/signup'}>Sign Up</Link>
<Link to={'/signin'}>Sign In</Link>
</ListItem>
</List>
</>
)}
</Container>
</Nav>
);
}
export { Navbar };
이런 코드가 있습니다. 여기서 고민은 조건부 랜더링이 나오면 바로 리팩토링 대상이라고 했습니다. 여기서 고민입니다.
- 다른 모듈에 담고 호출하는 것이 적절할까?
- 하위에 export 안하고 배치할까?
function
키워드의 장점을 활용하면 호이스팅 현상이 이점으로 작용할 수 있습니다. 제일 중요하고 외부에서 접근할 수 있는 함수를 최상단에 배치할 수 있습니다. 그리고 조건부 랜더링을 위해 있어야 할 함수는 하위에 유지할 수 있습니다. 즉 다른 모듈로 분리하지 않고 바로 관심사를 분리할 수 있습니다.- 여기서 더 고민해볼 수 있습니다. 다른 모듈로 분리해야 하는 이유는 무엇인가? 관심사의 분리가 이유인데 관심사는 마크업으로 정보를 어떤 관계로 어떻게 보여주는가? 이것이 관심사입니다. 그리고 마크업 내에서 주입받아야 할 로직이 없습니다. 다른 모듈로 분리했을 때 가독성을 저해할 것 같지 않습니다.
import { Link } from 'react-router-dom';
import { Nav, Container, List, ListItem } from './Navbar.style';
import { useLogin } from '../../hooks';
export function Navbar() {
const { isLoggedIn } = useLogin();
return <Nav>{isLoggedIn ? <LoggedInNav /> : <LoggedOutNav />}</Nav>;
}
function LoggedInNav() {
return (
<Container>
<List>
<ListItem>
<Link to={'/cards'}>Home</Link>
</ListItem>
<ListItem>
<Link to={'/deck'}>Deck</Link>
</ListItem>
</List>
<List>
<ListItem>
<Link to={'/setting'}>Setting</Link>
</ListItem>
</List>
</Container>
);
}
function LoggedOutNav() {
return (
<Container>
<List>
<ListItem>
<Link to={'/'}>Home</Link>
</ListItem>
</List>
<List>
<ListItem>
<Link to={'/signup'}>Sign Up</Link>
</ListItem>
<ListItem>
<Link to={'/signin'}>Sign In</Link>
</ListItem>
</List>
</Container>
);
}
- 일단 관심사를 분리하는 것은 올바른 선택은 맞았습니다.
fragment
에 의존하지 않게 되었습니다. - 순수하게 마크업만 담당하고 로직을 주입받지 않기 때문에 같은 모듈 내에 유지해도 괜찮을 것 같습니다. 로직을 주입받아야 하는 시점에 다른 모듈로 분리해도 늦을 것 같지 않습니다.
Nav 컴포넌트의 테스트 가능성
Cannot destructure property 'basename' of 'React__namespace.useContext(...)' as it is null.
Components/Navbar/index.tsx
Nav는 Layout
컴포넌트에 넣고 사용하고 있습니다. 문제는 테스트할 때 발생했습니다.
import { Global } from '@emotion/react';
import GlobalStyle from '../styles/Reset';
import { Navbar } from '../Components';
/**
* @see https://github.com/WANTED-TEAM03/pre-onboarding-10th-1-3/blob/main/src/routes/_globalLayout.tsx
*/
function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Global styles={GlobalStyle} />
<Navbar />
{children}
</>
);
}
export default Layout;
Nav가 정상동작하기 위해서는 개별 페이지별로 호출하는 방식으로 사용해야 한다는 것입니다.
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import Layout from './GlobalLayout';
import {
Cards,
Deck,
Landing,
NotFound,
Setting,
SignIn,
SignUp,
} from '../pages';
function Router() {
return (
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/cards" element={<Cards />} />
<Route path="/deck" element={<Deck />} />
<Route path="/setting" element={<Setting />} />
<Route path="/signin" element={<SignIn />} />
<Route path="/signup" element={<SignUp />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</BrowserRouter>
);
}
export default Router;
이렇게 컨텍스트 밖에 있는 것이 문제로 작용했습니다.
여기서 고민할 점은 당장해결할지 아니면 후순위로 두고 다른 가치가 더 높은 것에 대응할지입니다.
지금 시점에서는 기능 구현이 우선순위에서 높습니다. 따라서 추가 테스트 코드 작성 중에 혹은 리팩토링 진행 직전에 테스트를 추가합니다.