custom hook에서는 동등성 보장한 상태로 리턴해주기
export function IssueContextProvider({ children }: IssueContextProviderProps) {
const [issues, setIssues] = useState<IssueListResponseType['data']>([]);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const pageRef = useRef(1);
const isEndRef = useRef(false);
const getInfiniteIssues = useCallback(async () => {
if (isEndRef.current) return;
setIsLoading(true);
try {
const response = await RepositoryAPI.getIssueList(pageRef.current);
if (response.length === 0) {
isEndRef.current = true;
return;
}
pageRef.current = pageRef.current + 1;
setIssues((prev) => [...prev, ...response]);
} catch (error) {
setIsError(true);
} finally {
setIsLoading(false);
}
}, [RepositoryAPI]);
return (
<IssueContext.Provider
value={{ issues, isLoading, getInfiniteIssues, isError }}
>
{children}
</IssueContext.Provider>
);
}
의존성 드러내주기
const fetchIssue = async (id: string) => {
let response;
try {
if (id) {
response = await RepositoryAPI.getIssue(id);
setIssue(response);
setLoading(false);
}
} catch (error) {
return navigate('/error');
}
};
useEffect(() => {
if (id) {
fetchIssue(id);
}
}, [id]);
Try, Catch, Finally 속성 잘 이해하고 사용하기
const fetchIssueList = async () => {
try {
const currentPage = issueListPage;
handleLoading(true);
const newIssueList = await getIssueList(currentPage, 10);
setIssueListPage((prev) => prev + 1);
setIssueList((prevList) => [...prevList, ...newIssueList]);
} catch (error) {
const err = error as SystemError;
setFetchError(err.message);
} finally {
handleLoading(false);
}
};
코드의 결합은 신중하게 그리고 최대한 느슨하게
class HttpClient {
private BASE_URL = "<https://api.github.com>";
private TOKEN = process.env.REACT_APP_AUTHORIZATION;
protected axiosInstance = axios.create({
baseURL: this.BASE_URL,
headers: {
Authorization: this.TOKEN,
},
});
}
export class IssueService implements IssueServiceType {
private httpClient: HttpClient;
constructor(httpClient) {
this.httpClient = httpClient;
}
async getIssueList(pageNum: number) {
return await this.httpClient.axiosInstance
.get(
`/repos/facebook/react/issues?state=open&sort=comments&direction=desc&per_page=12&page=${pageNum}`
)
.then((response) => {
const status = response.status;
if (status === 200) {
const isEmpty = response.data.length === 0;
return !isEmpty ? extractIssueList(response.data) : false;
}
alertStatus(status, GET_ISSUE_LIST_STATUS);
})
.catch((error) => {
console.error("Error:", error);
});
}
async getIssueDetail(id: number) {
return await this.httpClient.axiosInstance
.get(`/repos/facebook/react/issues/${id}`)
.then((response) => {
const status = response.status;
if (status === 200) return extractIssueDetail(response.data);
alertStatus(status, GET_ISSUE_STATUS);
})
.catch((error) => {
console.error("Error:", error);
});
}
}
- 상속은 가장 강한 형태의 결합, 코드가 상호 결합된다는 것은 한쪽의 변화가 다른쪽에 영향을 미치게 된다는 것, 따라서 상속은 신중하게 사용해야 함
Class 내부에서만 사용하는 상수는 Class안으로 넣어주기
export class IssueApi {
private httpClient: HttpClient;
private readonly URL = '/repos/facebook/react';
constructor(httpClient: HttpClient) {
this.httpClient = httpClient;
}
async repository() {
const response = await this.httpClient.fetch(this.URL, {
method: 'GET',
});
return response.json();
}
}
- 클래스는 내부에 데이터를 가지고 있을 수 있기에, 굳이 바깥 범위인 파일에 놔두는 것 보다 내부에서만 사용되는 값이라면 내부로 가져와서 저장해두는 것이 효율적
- 이 과정에서
- 수정되지 않는 상수이기에 readonly,
- 외부로 노출될 필요가 없기에 private
CSS-in-JS와 className을 이용한 스타일링 혼용 금지
const IssueUlStyle = styled.ul`
width: 100%;
padding: 16px;
overflow-y: auto;
height: calc(100% - 62px);
position: relative;
.imptyImg {
width: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
`;
- styled-component를 이용해서 스타일링이 된 태그들은 해당 definition으로 이동해서 바로 스타일링을 확인이 가능한데 className으로 선언한 부분은 바로 definition으로 이동이 불가능해서 스타일링 파악하기에 어려움이 있음
- 위의 내용에 덧붙여서 className을 이용한 스타일링의 nesting depth가 깊어지는 경우에는 더 더욱 찾기가 힘들어짐
- depth가 깊어지는 경우 뿐만 아니라 1depth로도 className을 이용한 스타일 선언부가 길어지는 경우에도 해당 스타일링을 찾기 어려워짐
- 위의 사항들로 인해서 CSS in JS(styled-components 등)을 이용한 스타일링을 하는 경우에는 라이브러리 사용상 강제되는 등의 불가피한 부분을 제외하고는 className을 통한 스타일링은 최대한 지양하는게 권장됩니다.
tag selector 사용 지양
const IssueLiStyle = styled.li`
padding: 15px 10px;
border-radius: 10px;
a {
display: flex;
justify-content: space-between;
align-items: center;
img {
background-color: #fff;
height: 100%;
padding: 0 35%;
}
}
&:hover {
background-color: ${COLOR.DarkHover};
h3 {
text-decoration: underline;
color: ${COLOR.White};
}
}
`;
- a, img 태그 처럼 빈번히 사용되는 태그로 스타일을 주는 건 depth에 무관하게 적용되어 의도치 않은 스타일링이 될 수 있습니다.
- tag selector는 해당 요소 뿐만 아니라 동일한 태그로 선언된 다른 요소들에게도 영향을 미칠 여지가 너무 많기 때문에 추후 유지보수에 악영향을 미칠 수 있습니다.
의미가 드러나지 않는 값들 대신에 의미를 명확하게 알려주기
const isAdvertisement = isItMultipleOfFive(index+1);
isAdvertisement &&
nodes.push(
<li key={`ad#${index}`}>
<Advertisement />
</li>,
);
return nodes;
너무 많은 정보를 주는 것은 추상화가 제대로 되지 않은 것
function IssueList() {
const {
issueList: data,
isEnd,
countLoading,
fetchIssues,
fetchMoreIssues,
fetchIssueCount,
isLoading,
} = useIssues();
const target = useRef(null);
const [page, setPage] = useState(1);
function IssueList() {
const { data, error, loading, hasNextPage } = useIssueList();
const fetchNextPage = useIssueListDispatch();
const observeTargetRef = useInfiniteIssue<HTMLDivElement>();
동등성 제대로 보장해주기
const { issues, getIssues, error, loading, hasNextPage } = useIssues();
const getMoreIssues = useCallback(
async (entry, observer) => {
observer.unobserve(entry.target);
if (hasNextPage && !loading) {
getIssues();
}
},
[getIssues, hasNextPage, loading]
);
const ref = useObserver(getMoreIssues);
의존성 잘 넣어주기
const DetailPage = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
if (!id) {
navigate(PATH.ERROR_PAGE);
alert('페이지를 찾을 수 없습니다!');
}
const { detail, loading, loadIssueDetail } = useIssueDetail();
const { number, title, body, comments, updated_at, user } = detail;
useEffect(() => {
loadIssueDetail(Number(id));
}, [id, loadIssueDetail]);