12. 관리자 페이지 구성하기 ver.2
테이블 컴포넌트 다시 만들어보기
이전 포스트에서 🔥 표시로 작성해놓은 부분을 계속 생각하다가,
테이블 정렬 기능을 포함하는 김에 JS를 TS로 바꾸고, 테이블 컴포넌트 자체에서 백엔드에 데이터를 요청하도록 수정하려고 했다.
아무리 생각해도 토큰을 받고 -> 데이터를 받아서 -> 테이블 컴포넌트에 매개변수로 주고 -> 이를 출력하는 과정 자체가,
토큰을 받아 테이블 컴포넌트에게 주고 -> 테이블 컴포넌트에서 데이터를 받아 출력하는 것보다 비효율적인 것 같아서…
Typescript로 다시 작성하기
일단 토큰을 백엔드에 요청하고, 테이블 컴포넌트를 호출했던 상위 컴포넌트 SectionClassForInst
에서,
토큰을 기반으로 채점 결과 데이터를 가져오는 함수를 가져온다.
또한, null
, undefined
일 경우와 고려하지 않아도 될 키들을 정리하는 함수까지 ~뺏어~가져온다
기존에는 자바스크립트로 작성되어 있으니, 타입을 잘 확인하면서 다시 작성해보자.
// file: 'SectionTable.tsx'
export default function SectionTable(props: ClassroomInstTokenProps) {
const classes = useStyles();
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [keyGroup, setKeyGroup] = useState<string[]>( [] );
const [dataGroup, setDataGroup] = useState<GradingResultProps[]>( [] );
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value));
setPage(0);
};
const emptyRows = rowsPerPage - Math.min(rowsPerPage, dataGroup.length - page * rowsPerPage);
useEffect(() => {
const getGradingData = async () : Promise<GradingResultProps[]> => {
return await axios.get<GradingResultProps[]>('/api/grade/', {
params: {
itoken: props.itoken
},
}).then((response) => {
return response.data;
});
}
getGradingData()
.then((response) => {
var i = 0;
for (; i < response.length; i++) {
const element = clean(response[i]) as GradingResultProps;
if (i === 0) {
setKeyGroup(Object.keys(element).filter(item =>
item !== 'studentNum' && item !== 'point' && item !== 'result' &&
item !== 'gradingDate' && item !== 'className'));
}
setDataGroup(old => [ ...old, element]);
}
})
}, [props.itoken]);
테이블 컴포넌트에서 데이터를 요청하며, itoken에 대한 변경사항이 있을 때 실행하도록 한다. (두 번째 인수를 주지 않으면 변경사항이 있을 때마다 effect가 실행되는 대참사가 일어난다💦)
미리 정의한 GradingResultProps
인터페이스 타입으로 Promise 객체를 호출하며,
데이터를 받아오면 clean
함수를 거쳐 필요없는 키를 제거하는 작업을 거친다.
데이터는 그럴 필요가 없으니 그대로 업데이트 시켜주면 된다.
처음 unknown
타입을 작성한 코드인데, unknown
이 뭐냐? (처음엔 any
가 만능인 줄 알았는데?)
unknown
도 모든 타입을 받을 수 있지만, any
처럼 모든 타입을 가지면서 안전하지 않은 타입과는 차이점이 있다.
any
를 제외한 다른 타입으로 선언된 변수에는, unknown
타입의 변수를 할당할 수 없다.
따라서 any
를 최대한 덜 쓰고도 모든 타입을 받을 수 있다.
테이블 정렬 기능 만들기
백엔드에서 요청한 데이터베이스는 주로 시간 상으로 정렬되어 있었다
이 리스트들을 특정 어트리뷰트로 정렬하면, 훨씬 보기 좋을 것 같다.
특히 학번, 점수로 오름차, 내림차순 정렬을 지원하면 나중에 문서화하기도 굉장히 편하고.
함수를 하나하나씩 선언해보자.
// file: 'SectionTable.tsx'
...
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
...
Java에서 많이 보던 Generic
타입으로 선언한 함수다.
이렇게 제네릭으로 구성하면 any
를 쓰지 않고도 타입을 유연하게 받을 수 있으며
any
를 사용했을 때 타입이 일치하지 않을 위험 또한 방지할 수 있다.
a
와 b
는 정렬할 데이터의 타입 (GradingResultProps
)을 가지고 있을 것이다.
// file: 'SectionTable.tsx'
type Order = 'asc' | 'desc';
...
function getComparator<Key extends keyof any>(
order: Order,
orderBy: Key,
): (a: { [key in Key]: number | string }, b: { [key in Key]: number | string }) => number {
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}
...
내림차순인지, 오름차순인지 판별하기 위해 Order
타입을 선언해 준다.
getComparator
함수는 현재 order
과 (정렬 기준이 되는) 키의 이름을 받으며,
이 키의 이름은 문자열 또는 수가 될 수 있다.
order
이 내림차순이면 위에 정의한 함수를 그대로 호출하며, 오름차순이라면 1과 -1를 역수 취해주면 된다.
// file: 'SectionTable.tsx'
...
function stableSort<T>(array: T[], comparator: (a: T, b: T) => number) {
const stabilizedThis = array.map((element, index) => [element, index] as [T, number]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0)
return order;
return a[1] - b[1];
});
return stabilizedThis.map((element) => element[0]);
}
...
실제로 정렬을 수행할 stableSort
함수다.
정렬할 값 리스트를 받을 array
, 위에서 정의한 오름차, 내림차순에 따라 -1, 0, 1을 반환할 comparator
를 매개변수로 받는다.
이 array의 값을 직접 변경하지 않으면서 (불변성을 위해)
두 개의 값을 내장함수에서 판별하도록 한다.
현재 함수는 오름차순으로 동작하도록 정의되었다.
(처음 비교 대상이 같으면 바로 오름차순으로 내장함수가 동작하도록)
이후 정렬된 배열을 0번째부터 리턴해 준다.
다음으로, 정렬 기준이 되는 헤더를 눌렀을 때 즉시 정렬될 수 있도록, TableHead
컴포넌트를 수정한다.
// file: 'index.d.ts'
...
export interface EnhancedTableProps {
order: Order,
orderBy: string,
keyGroup: string[],
onRequestSort: (event: React.MouseEvent<unknown>, property: keyof ResultKeyProps) => void,
}
...
재작성할 헤더 타입을 위해 인터페이스를 정의한다.
왜냐, 기존에 Table
컴포넌트의 자식으로 저 위의 변수들을 받을 수 있었는데,
분리하면서 매개변수로 받아야 하기 때문이다.
...
function EnhancedTableHead(props: EnhancedTableProps) {
const { order, onRequestSort, keyGroup } = props;
const createSortHandler = (property: keyof ResultKeyProps) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property);
};
return (
<TableHead>
<TableRow>
<StyledTableCell key="stn" align="right" sortDirection={order} >
<TableSortLabel
active={true}
direction={order}
onClick={createSortHandler('studentNum')}
style=
>
Student Number
</TableSortLabel>
</StyledTableCell>
<StyledTableCell key="total" align="right">
Total Score
</StyledTableCell>
<StyledTableCell key="res" align="right" sortDirection={order} >
<TableSortLabel
active={true}
direction={order}
onClick={createSortHandler('result')}
style=
>
Student Score
</TableSortLabel>
</StyledTableCell>
<StyledTableCell key="time" align="right">
Grading Timestamp
</StyledTableCell>
<StyledTableCell key="cname" align="right">
Class name
</StyledTableCell>
{keyGroup.map((row, index) => (
<StyledTableCell key={index} align="right">
{row}
</StyledTableCell>
))}
</TableRow>
</TableHead>
)
}
...
기존 Table
이 있는 함수형 컴포넌트에서 onRequestSort
함수를 넘겨줄 건데,
이를 받은 쪽에서 호출하면 넘겨준 함수가 동작하는 방식이다.
keyGroup
은 넘겨준 쪽에서 필요없는 키 값을 모두 필터링하고 출력하기 위해 넘겨준 것들이다.
마지막으로, 위 컴포넌트를 호출할 컴포넌트에도 수정해주면 된다.
// file: 'SectionTable.tsx'
...
export default function SectionTable(props: ClassroomInstTokenProps) {
const classes = useStyles();
const [order, setOrder] = useState<Order>('asc'); // NEW!!
const [orderBy, setOrderBy] = useState<keyof ResultKeyProps>('studentNum'); // NEW!!
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [keyGroup, setKeyGroup] = useState<string[]>( [] );
const [dataGroup, setDataGroup] = useState<GradingResultProps[]>( [] );
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value));
setPage(0);
};
// 이 함수가 onRequestSort 이름으로 넘겨줄 정렬 기준선택 함수다!
const handleRequestSort = (event: React.MouseEvent<unknown>, property: keyof ResultKeyProps) => {
const isAsc = orderBy === property && order === 'asc';
setOrder(isAsc ? 'desc' : 'asc');
setOrderBy(property);
}
...
return (
<Paper className={classes.root}>
<TableContainer component={Paper} className={classes.container}>
<Table stickyHeader>
<EnhancedTableHead
order={order}
orderBy={orderBy}
onRequestSort={handleRequestSort}
keyGroup={keyGroup}
/>
<TableBody>
{stableSort(dataGroup, getComparator(order, orderBy))
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row, index) => {
return (
<StyledTableRow key={index + `v`}>
<TableCell key={index + `stn`} align="right">
{row['studentNum']}
</TableCell>
<TableCell key={index + `total`} align="right">
{row.point}
...
내림차순, 오름차순 선택을 위한 order
, 정렬 기준 키인 orderBy
변수를 선언하고,
handleRequestSort
함수를 onRequestSort
함수로 넘겨줘 정렬 기준을 동적으로 변경하도록 한다.
이 기준을 바탕으로 정렬된 값 리스트들은 stableSort
함수에서 리턴되며,
이를 컴포넌트로 리턴해주면 된다!😆
🔥 TroubleShooting & Review
Issue:
stableSort
함수에서 리턴된 값들을 각TableCell
의 자식으로 넘겨주려고 했는데,row[detail]
과 같이 작성했더니 오류를 뱉었다. String 타입은 배열의 index로 사용할 수 없다는 에러였는데, 인터페이스GradingResultProps
에서export interface GradingResultProps: { [key: string]: GradingResultProps, ...
항목을 추가하고 String 타입인
detail
을 primitive한string
으로 변경했더니 해결되었다. (이것 때문에 구글과 몇 시간을 씨름했는지…🥺)- 제네릭은 JS/TS에서 처음 써보는 것 같다. Java에서 써본 적은 있지만 여기서도 동일하게 지원할 줄은 몰랐다… (처음에 any를 때려박으면서 굉장히 불편했다는 여담)
Review
- JS에서는
row[detail].deductedPoint
와 같이 타입에 상관없이 적어도 굉장히 유연하게 동작했는데, TS로 변경하면서 굉장히 삽질을 오래 했다. 그리고 마침내 해결…! - 테이블 정렬 기능은 꼭 넣어야지 넣어야지 했는데 핫픽스가 생겨서 계속 미뤄진 기능이었다. 깃허브의 이슈 Resolve 커밋 적을 때 굉장히 뿌듯했다✨