diff --git a/app/(protected)/(tabs)/_layout.tsx b/app/(protected)/(tabs)/_layout.tsx
index 9b91c20..1257b12 100644
--- a/app/(protected)/(tabs)/_layout.tsx
+++ b/app/(protected)/(tabs)/_layout.tsx
@@ -1,5 +1,5 @@
import { Tabs } from "expo-router";
-import { Platform, Text, View } from "react-native";
+import { DeviceEventEmitter, Platform, Text, View } from "react-native";
import { HeaderWithBack, HeaderWithNotification } from "@/components/Header";
import useSubscribeNotification from "@/hooks/useSubscribeNotification";
@@ -81,6 +81,14 @@ export default function TabsLayout() {
title: "Home",
tabBarIcon: ({ color }) => ,
}}
+ listeners={({ navigation, route }) => ({
+ tabPress: () => {
+ const state = navigation.getState();
+ if (state.routes[state.index].name === route.name) {
+ DeviceEventEmitter.emit("SCROLL_HOME_TO_TOP");
+ }
+ },
+ })}
/>
,
}}
+ listeners={({ navigation, route }) => ({
+ tabPress: () => {
+ const rootState = navigation.getState();
+ if (rootState.routes[rootState.index].name !== route.name) return;
+
+ const nestedState = rootState.routes[rootState.index].state as {
+ index: number;
+ routeNames: string[];
+ };
+ if (!nestedState?.routeNames) {
+ DeviceEventEmitter.emit("SCROLL_FRIEND_TO_TOP");
+ return;
+ }
+
+ const topTabName = nestedState.routeNames[nestedState.index];
+ const eventMap = {
+ index: "SCROLL_FRIEND_TO_TOP",
+ request: "SCROLL_REQUEST_TO_TOP",
+ };
+
+ const eventName = eventMap[topTabName as keyof typeof eventMap];
+ if (eventName) {
+ DeviceEventEmitter.emit(eventName);
+ }
+ },
+ })}
/>
,
}}
+ listeners={({ navigation, route }) => ({
+ tabPress: () => {
+ const state = navigation.getState();
+ if (state.routes[state.index].name === route.name) {
+ DeviceEventEmitter.emit("SCROLL_MY_PAGE_TO_TOP");
+ }
+ },
+ })}
/>
>
diff --git a/app/(protected)/(tabs)/friend/index.tsx b/app/(protected)/(tabs)/friend/index.tsx
index d8c945b..f27dd2c 100644
--- a/app/(protected)/(tabs)/friend/index.tsx
+++ b/app/(protected)/(tabs)/friend/index.tsx
@@ -1,5 +1,5 @@
import { useQueryClient } from "@tanstack/react-query";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import ErrorScreen from "@/components/ErrorScreen";
import { FriendItem } from "@/components/FriendItem";
@@ -23,6 +23,7 @@ export default function Friend() {
isFetchingNextPage,
error,
loadMore,
+ refetch,
} = useInfiniteLoad({
queryFn: getFriends(keyword),
queryKey: ["friends", keyword],
@@ -35,11 +36,13 @@ export default function Friend() {
}, 200);
// 친구창에 focus 들어올 때마다 친구목록 새로고침 (검색중일 때 제외)
- useFocusEffect(() => {
- if (!keyword && !isFetchingNextPage) {
- queryClient.invalidateQueries({ queryKey: ["friends"] });
- }
- });
+ useFocusEffect(
+ useCallback(() => {
+ if (!keyword && !isFetchingNextPage) {
+ queryClient.invalidateQueries({ queryKey: ["friends"] });
+ }
+ }, [queryClient, keyword, isFetchingNextPage]),
+ );
// 친구의 운동 정보가 바뀌면 쿼리 다시 패치하도록 정보 구독
useEffect(() => {
@@ -80,6 +83,7 @@ export default function Friend() {
return (
(null);
// 유저의 친구 요청 정보 조회
const {
@@ -28,6 +34,7 @@ export default function Request() {
isFetchingNextPage,
error,
loadMore,
+ refetch,
} = useInfiniteLoad({
queryFn: getFriendRequests,
queryKey: ["friendRequests"],
@@ -36,11 +43,13 @@ export default function Request() {
const hasRequests = !!requestData?.pages[0].total;
// 친구 요청창에 focus 들어올 때마다 친구목록 새로고침
- useFocusEffect(() => {
- if (!isFetchingNextPage) {
- queryClient.invalidateQueries({ queryKey: ["friendRequests"] });
- }
- });
+ useFocusEffect(
+ useCallback(() => {
+ if (!isFetchingNextPage) {
+ queryClient.invalidateQueries({ queryKey: ["friendRequests"] });
+ }
+ }, [isFetchingNextPage, queryClient]),
+ );
// 친구 요청이 추가되면 쿼리 다시 패치하도록 정보 구독
useEffect(() => {
@@ -60,6 +69,20 @@ export default function Request() {
};
}, [queryClient.invalidateQueries]);
+ const handleScrollToTop = useCallback(() => {
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
+ refetch();
+ }, [refetch]);
+
+ useEffect(() => {
+ const subscription = DeviceEventEmitter.addListener(
+ "SCROLL_REQUEST_TO_TOP",
+ handleScrollToTop,
+ );
+
+ return () => subscription.remove();
+ }, [handleScrollToTop]);
+
// 에러 스크린
if (error) {
return ;
@@ -73,6 +96,7 @@ export default function Request() {
return (
page.data) : []}
keyExtractor={(request) => String(request.requestId)}
diff --git a/app/(protected)/(tabs)/home.tsx b/app/(protected)/(tabs)/home.tsx
index 3b78e6f..e046a11 100644
--- a/app/(protected)/(tabs)/home.tsx
+++ b/app/(protected)/(tabs)/home.tsx
@@ -11,9 +11,10 @@ import useRefresh from "@/hooks/useRefresh";
import { getPostLikes, getPosts } from "@/utils/supabase";
import { useRouter } from "expo-router";
import * as SecureStore from "expo-secure-store";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
+ DeviceEventEmitter,
Dimensions,
FlatList,
Image,
@@ -33,6 +34,7 @@ export default function Home() {
const [selectedPostId, setSelectedPostId] = useState(null);
const [selectedAuthorId, setSelectedAuthorId] = useState(null);
const [isLikedModalVisible, setIsLikedModalVisible] = useState(false);
+ const flatListRef = useRef(null);
const { openModal } = useModal();
const router = useRouter();
@@ -91,9 +93,24 @@ export default function Home() {
handleLoadId();
}, []);
+ const handleScrollToTop = useCallback(() => {
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
+ onRefresh();
+ }, [onRefresh]);
+
+ useEffect(() => {
+ const subscription = DeviceEventEmitter.addListener(
+ "SCROLL_HOME_TO_TOP",
+ handleScrollToTop,
+ );
+
+ return () => subscription.remove();
+ }, [handleScrollToTop]);
+
return (
page.data) ?? []}
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={{ gap: 10 }}
diff --git a/app/(protected)/(tabs)/mypage.tsx b/app/(protected)/(tabs)/mypage.tsx
index 8a6f0d9..29b1fb5 100644
--- a/app/(protected)/(tabs)/mypage.tsx
+++ b/app/(protected)/(tabs)/mypage.tsx
@@ -20,6 +20,7 @@ export default function MyPage() {
data: posts,
isLoading: isPostsLoading,
isError: isPostsError,
+ refetch,
} = useFetchData(
["userPosts"],
() => getMyPosts(),
@@ -43,6 +44,7 @@ export default function MyPage() {
}
/>
({ ...post, id: post.id.toString() }))
diff --git a/components/PostGrid.tsx b/components/PostGrid.tsx
index 9d8f1f7..b355748 100644
--- a/components/PostGrid.tsx
+++ b/components/PostGrid.tsx
@@ -1,6 +1,8 @@
import images from "@/constants/images";
import { useRouter } from "expo-router";
+import { useCallback, useEffect, useRef } from "react";
import {
+ DeviceEventEmitter,
Dimensions,
FlatList,
Image,
@@ -14,12 +16,28 @@ interface Post {
}
interface PostGridProps {
+ refetch: () => void;
posts: Post[] | null;
isError?: boolean;
}
-export default function PostGrid({ posts, isError }: PostGridProps) {
+export default function PostGrid({ refetch, posts, isError }: PostGridProps) {
const router = useRouter();
+ const flatListRef = useRef(null);
+
+ const handleScrollToTop = useCallback(() => {
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
+ refetch();
+ }, [refetch]);
+
+ useEffect(() => {
+ const subscription = DeviceEventEmitter.addListener(
+ "SCROLL_MY_PAGE_TO_TOP",
+ handleScrollToTop,
+ );
+
+ return () => subscription.remove();
+ }, [handleScrollToTop]);
if (isError) {
return (
@@ -52,6 +70,7 @@ export default function PostGrid({ posts, isError }: PostGridProps) {
return (
{
const size = Dimensions.get("window").width / 3;
diff --git a/components/SearchLayout.tsx b/components/SearchLayout.tsx
index 352aeca..e447d1f 100644
--- a/components/SearchLayout.tsx
+++ b/components/SearchLayout.tsx
@@ -1,8 +1,9 @@
import colors from "@/constants/colors";
import type { UserProfile } from "@/types/User.interface";
-import { useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
+ DeviceEventEmitter,
FlatList,
type ListRenderItemInfo,
View,
@@ -11,6 +12,7 @@ import { SafeAreaView } from "react-native-safe-area-context";
import SearchBar from "./SearchBar";
interface SearchLayoutProps {
+ refetch?: () => void;
data: UserProfile[]; // 추후 검색 사용 범위 넓어지면 변경 가능
isFetchingNextPage?: boolean;
onChangeKeyword: (newKeyword: string) => void;
@@ -20,6 +22,7 @@ interface SearchLayoutProps {
}
export function SearchLayout({
+ refetch,
data,
isFetchingNextPage,
onChangeKeyword,
@@ -28,10 +31,26 @@ export function SearchLayout({
emptyComponent,
}: SearchLayoutProps) {
const [keyword, setKeyword] = useState("");
+ const flatListRef = useRef(null);
+
+ const handleScrollToTop = useCallback(() => {
+ flatListRef.current?.scrollToOffset({ offset: 0, animated: true });
+ refetch?.();
+ }, [refetch]);
+
+ useEffect(() => {
+ const subscription = DeviceEventEmitter.addListener(
+ "SCROLL_FRIEND_TO_TOP",
+ handleScrollToTop,
+ );
+
+ return () => subscription.remove();
+ }, [handleScrollToTop]);
return (
elem.id}