- webpack과 babel을 활용한 기본적인 프론트엔드 개발 환경 구축
- Redux를 사용한 상태 관리
- Jest, React-Testing-Library를 사용한 TDD 방식으로 개발 진행 중
📎 Front-end
- React.js(v18)
- react-router-dom(v6)
- Redux Toolkit
- Jest / React Testing Library
- Emotion
📎 Back-end
- Firebase(REST)
- test coverage가 100%에 도달할 때만 commit을 하는 방식으로 프로젝트를 진행 중
🔗 addEventListener의 테스트 코드 만들기
// MembershipPage.test.jsx
describe('MembershipPage', () => {
beforeEach(() => {
useSelector.mockImplementation((selector) => selector({
// ......
}));
});
describe('addEventListener', () => {
const handleClick = jest.fn();
beforeEach(() => {
handleClick.mockClear();
});
const mockEvents = () => {
const events = {};
EventTarget.prototype.addEventListener = jest.fn(
(event, callback) => {
events[event] = callback;
},
);
EventTarget.prototype.removeEventListener = jest.fn(
(event) => {
delete events[event];
},
);
return events;
};
context('when clicked inside the modal', () => {
it("doesn't close the modal window", () => {
const events = mockEvents();
const { getByTestId } = render(
<MembershipPage onClick={handleClick} />,
);
events.mousedown(
{ target: getByTestId('LoginPage') },
);
expect(handleClick).toBeCalledTimes(0);
});
});
});
});모달 창 내부를 클릭했을 때는, 해당 모달 창이 닫히지 않도록 하는 액션을 테스트했습니다.
- addEventListener를 모의 함수로 만들고, mosedown 메서드를 실행시켜 이벤트 핸드러가 실행되었을 때 props로 전달받은 handleClick 함수가 실행되지 않는 것으로 테스트를 검증했습니다.
🔗 Jest로 error 검증하기
💡 gunhee’s coding blog: Jest로 error 검증하기
// api.test.js
describe('api', () => {
const mockFetch = (data) => {
global.fetch = jest.fn().mockResolvedValue({
async json() { return data; },
});
};
describe('postLogin', () => {
context('when login fails', () => {
beforeEach(() => {
mockFetch(LOGIN_FAIL_DATA);
});
it('throw an error', async () => {
await expect(async () => {
await postLogin({
email: '[email protected]',
password: '',
});
}).rejects.toThrowError(new Error('INVALID_PASSWORD'));
});
});
});
});expect.rejects.toThrowError() matcher를 사용하여, 비동기 함수에서 예상한 오류가 발생하는지 확인하였습니다.
- 회원가입 시 14개의 조건문과 정규 표현식을 적용하여 4개의 input창을 관리하도록 구현
🔗 onblur와 onChange를 사용한 유효성 검사
// MembershipContainer.jsx
export default function MembershipContainer() {
const handleChange = useCallback(
({ name, value }) => {
dispatch(changeAccountFields({ name, value }));
dispatch(checkMemberInfo({ name, value }));
},
[dispatch],
);
const handleBlur = useCallback(
({ name, value }) => {
if (isButtonActive) return;
dispatch(checkInputValue({ name, value }));
},
[dispatch, isButtonActive],
);
return (
<>
<MembershipForm
onChange={handleChange}
onBlur={handleBlur}
/>
// ......
</>
);
}- handleChange() 함수에서는 회원가입 시, 4가지 항목(성, 이름, Email, Password)의 value를 확인하여 모두 유효성을 통과한다면 buttonActive의 값은 true로 변경하여 ‘Sign Up’ 버튼을 활성화합니다.
- handleBlur() 함수에서는 blur 이벤트가 발생 시, focus가 잡혀있던 input의 value를 확인하여 유효성이 맞지 않다면 그에 맞는 validation message를 생성합니다.
💡 gunhee’s coding blog: Login 유효성 검사 함수 리팩터링
- 사용자가 선택한 슬라이드의 data를 state로 관리하여 앨범이 자동으로 함께 움직이도록 구현
💡 gunhee’s coding blog: 라이브러리 없이, 무한 슬라이드 구현하기
🔗 리스트 페이지에서 공용으로 적용되는 Card 컴포넌트 제작
// ItemCard.jsx
const Wrapper = styled.div(({ hoverColor }) => ({
position: 'relative',
// ......
marginLeft: '3%',
marginBottom: '3%',
'&:hover': {
'h3, span': {
backgroundColor: hoverColor
},
},
}));
export default function ItemCard({
product: {
id, title, // .......
},
}) {
function makeHoverColor() {
const number = id % 10;
return itemCardHoverList[number];
}
return (
<Wrapper hoverColor={makeHoverColor()}>
// ......
</Wrapper>
);
};- hoverColor를 만들고, Wrapper 객체에 props로 전달하여 hover 시 지정된 color로 변경되도록 구현했습니다.
- Wrapper 객체에 left와 bottom에 margin 값을 넣어서, map 메서드로 화면에 렌더링 될 때 자동으로 좌우 간격이 맞춰지도록 스타일을 적용했습니다.
- 첫 렌더링 시, 각 옵션의 첫 번째 버튼이 자동 선택되도록 구현
- 공용 컴포넌트로 사용하기 위해, emotion 객체에 props를 전달하여, value에 따라 각각 다르게 스타일링이 적용되도록 구현
💡 GitHub 코드 바로가기: DetailOptionButton.jsx
-
함수 로직 추가 시에도 코드의 유지 보수성을 위해, 쿼리 스트링을 반복문으로 구현
// urlStates의 예시 // const urlState= { // product: 'sofas', // category: 'new', // sort: 'trending', // material: 'fabric', // }; function makeSearch() { const searchEntries = Object.entries(urlStates) .filter(([categoryName]) => url.searches.includes(categoryName)); searchEntries.forEach(([categoryName, categoryValue]) => { if (!categoryValue) return; if (search.length) { search.push(`&${categoryName}=${categoryValue}`); return; } search.push(`?${categoryName}=${categoryValue}`); }); }









